本文已参与「新人创作礼」活动,一起开启掘金创作之路。
"If you can't fly then run, if you can't run then walk, if you can't walk then crawl, but whatever you do you have to keep moving forward." (Martin Luther King)
1 说明
关于状态机的背景知识,在上一篇文章什么是状态机?一篇文章就够了,已经有做介绍,还没有看的小伙伴可以先点击进去看一下,如果实在没有时间看,也不影响本篇文章的阅读,本篇文章主要讲一个状态机c语言实现框架,和基于该框架的一些实例。
2 状态机框架
stateMachine状态机框架 是github上的一个开源项目,采用的是MIT License,用c语言编写,核心代码不过150行,很值得参考和学习。
传送门: github.com/misje/state…
说明文档: misje.github.io/stateMachin…
2.1 框架特点
stateMachine状态机框架 具有功能丰富、代码精练的特点(核心代码不到150行),其支持状态守护、转移守护、事件能携带数据、进入动作(entry action)、退出动作(exit action)、转移动作(transition action) 、状态数据自定义和 父状态/子状态及其嵌套。
如下为stateMachine状态机框架说明图。每一个状态都可以定义自己的entryAction()和exitAction();每一个transition都可以定义自己的guard()和action();不同的状态可以有共同的父状态;也可以不定义guard(),将transition变为无条件转移类型。
2.2 框架分析
2.2.1 数据结构
stateMachine状态机框架,有四个重要的结构体struct event,struct transition,struct state,struct stateMachine。已经都对其做了中文注释,其定义分别如下:
struct event
{
/*事件类型,这个自定义,一般定义成枚举类型*/
int type;
/*事件所携带的数据,类型自己定义,一般定义为结构体,便于扩展*/
void *data;
};
struct transition
{
/*触发转移的事件,其值应该为struct event结构体中type类型*/
int eventType;
/*事件event必须满足condition才会发生转移,它会作为guard函数的参数向下传递*/
void *condition;
/*该函数称为守护函数,自己定义,一般会将condition和事件event所携带的data
**进行比较,输出一个bool类型的值,决定是否发生转移*/
bool ( *guard )( void *condition, struct event *event );
/*当转移发生了,一些需要做的事情,可以放在这里去执行,其传入参数是
当前状态的数据和将要到达状态的数据以及事件,此函数也可以为空。*/
void ( *action )( void *currentStateData, struct event *event, void*newStateData );
/*将要转移过去的状态*/
struct state *nextState;
};
struct state
{
/*当前定义状态的父状态,如果当前定义状态有父状态,这个指针要是非空*/
struct state *parentState;
/*如果当前定义的状态有子状态,此指针应该指向作为入口点的子状态*/
struct state *entryState;
/*关于此状态转移的数组,其定义了此状态的转移流程*/
struct transition *transitions;
/*此状态的转移个数,它为上面数组的个数大小*/
size_t numTransitions;
/*状态所携带的数据,用作entryAction() 、exitAction()和
transition action()的参数*/
void *data;
/*进入该状态时会被回调的函数,自己定义,也可以为空*/
void ( *entryAction )( void *stateData, struct event *event );
/*退出该状态时会被回调的函数,自己定义,也可以为空*/
void ( *exitAction )( void *stateData, struct event *event );
};
struct stateMachine
{
/*指针所指向的当前状态*/
struct state *currentState;
/*指向上一状态的指针,方便跟踪记录历史状态*/
struct state *previousState;
/*指向系统中发生异常的状态*/
struct state *errorState;
};
2.2.2 函数接口
stateMachine状态机框架所有接口有如下五个stateM_init(),stateM_handleEvent(),stateM_currentState(),stateM_previousState(),stateM_stopped(),其中最重要的接口是stateM_handleEvent(),它处理了状态机最核心的流程。接口已经如下罗列了出来,加上了中文注释,现在重点分析stateM_handleEvent()的内部流程。
/*状态机初始化接口,需要传入初始状态和异常状态,通过指针返回状态机*/
void stateM_init( struct stateMachine *stateMachine,
struct state *initialState, struct state *errorState );
/*将事件传入进状态机,因为事件会源源不断的来,此接口会被不断调用*/
int stateM_handleEvent( struct stateMachine *stateMachine,
struct event *event );
/*获取系统当前状态,因为在状态机中有记录很好拿*/
struct state *stateM_currentState( struct stateMachine *stateMachine );
/*获取系统上一个状态,因为在状态机中有记录很好拿*/
struct state *stateM_previousState( struct stateMachine *stateMachine );
/*停止状态机*/
bool stateM_stopped( struct stateMachine *stateMachine );
先来看一下stateM_handleEvent()接口的返回值类型,它的返回值是一个枚举类型,总共定义了6种。枚举的含义已经在如下做了注解。stateM_handleEvent()的调用流程大概可以描述如下:
- 判断入参
fsm和event是否为空,为空时直接返回stateM_errArg; - 判断当前状态是否为空,如果为空,将当前状态置为
errorState,返回stateM_errorStateReached; - 判断当前状态的转移数组
numTransitions大小是否为零,如果为零,返回stateM_noStateChange; - 获取当前状态的
transition,如果当前状态没有transition,则获取父状态的transition,如果往上遍历父状态transition都没有,直到父状态为空,返回stateM_noStateChange; - 判断步骤(4.)拿到的
transition,是否有nextState,如果为空,将当前状态置为errorState,返回stateM_errorStateReached; - 拿到步骤(5.)中的
nextState,向下遍历nextState的entryState,直到nextState没有entryState,然后将此刻的nextState,作为将要转移过去的状态; - 如果当前状态和将要转移过去的状态不相等,则回调
exitAction()函数; - 回调
transition的action()函数; - 如果当前状态和将要转移过去的状态不相等,则回调
entryAction()函数; - 如果当前状态和将要转移过去的状态相等,返回
stateM_stateLoopSelf; - 如果当前状态等于
errorState,则返回stateM_errorStateReached; - 如果将要转移状态的转移数组
numTransitions大小为零,则返回stateM_finalStateReached; - 如果上面都没有遇到异常,则流程执行完成,返回
stateM_stateChanged。
enum stateM_handleEventRetVals
{
/*参数传递错误*/
stateM_errArg = -2,
/*到达一个异常状态时,会返回该值;当前状态为空,
**transition中的nextState为空也会返回该值*/
stateM_errorStateReached,
/*当前状态改变了,并且进入的是非最终状态,会返回该值*/
stateM_stateChanged,
/*从当前状态又回到了当前状态会返回该值*/
stateM_stateLoopSelf,
/*所传递事件没有导致当前状态改变*/
stateM_noStateChange,
/*到达了最终状态(异常状态除外)*/
stateM_finalStateReached,
};
int stateM_handleEvent( struct stateMachine *fsm,
struct event *event )
{
if ( !fsm || !event )
return stateM_errArg;
if ( !fsm->currentState )
{
goToErrorState( fsm, event );
return stateM_errorStateReached;
}
if ( !fsm->currentState->numTransitions )
return stateM_noStateChange;
struct state *nextState = fsm->currentState;
do {
struct transition *transition = getTransition( fsm, nextState, event );
/* If there were no transitions for the given event for the current
* state, check if there are any transitions for any of the parent
* states (if any): */
if ( !transition )
{
nextState = nextState->parentState;
continue;
}
/* A transition must have a next state defined. If the user has not
* defined the next state, go to error state: */
if ( !transition->nextState )
{
goToErrorState( fsm, event );
return stateM_errorStateReached;
}
nextState = transition->nextState;
/* If the new state is a parent state, enter its entry state (if it has
* one). Step down through the whole family tree until a state without
* an entry state is found: */
while ( nextState->entryState )
nextState = nextState->entryState;
/* Run exit action only if the current state is left (only if it does
* not return to itself): */
if ( nextState != fsm->currentState && fsm->currentState->exitAction )
fsm->currentState->exitAction( fsm->currentState->data, event );
/* Run transition action (if any): */
if ( transition->action )
transition->action( fsm->currentState->data, event, nextState->
data );
/* Call the new state's entry action if it has any (only if state does
* not return to itself): */
if ( nextState != fsm->currentState && nextState->entryAction )
nextState->entryAction( nextState->data, event );
fsm->previousState = fsm->currentState;
fsm->currentState = nextState;
/* If the state returned to itself: */
if ( fsm->currentState == fsm->previousState )
return stateM_stateLoopSelf;
if ( fsm->currentState == fsm->errorState )
return stateM_errorStateReached;
/* If the new state is a final state, notify user that the state
* machine has stopped: */
if ( !fsm->currentState->numTransitions )
return stateM_finalStateReached;
return stateM_stateChanged;
} while ( nextState );
return stateM_noStateChange;
}
现在讨论一下getTransition()函数接口。它在stateM_handleEvent()的调用流程中,获取当前状态的transition时被调用到。从该函数的调用过程可以很清楚的看出来,通过传入的event中的type来和当前状态的transitions数组进行遍历比对,直到发现那个匹配的transition,如果传入的event和匹配到的transition,满足守护函数guard(),则返回transitions,否则返回NULL
至此,stateMachine状态机框架的核心调用流程介绍完毕,下一章节介绍一下对应的应用实例。
static struct transition *getTransition( struct stateMachine *fsm,
struct state *state, struct event *const event )
{
size_t i;
for ( i = 0; i < state->numTransitions; ++i )
{
struct transition *t = &state->transitions[ i ];
/* A transition for the given event has been found: */
if ( t->eventType == event->type )
{
if ( !t->guard )
return t;
/* If transition is guarded, ensure that the condition is held: */
else if ( t->guard( t->condition, event ) )
return t;
}
}
/* No transitions found for given event for given state: */
return NULL;
}
3 应用实例
本次实例用实际开发过程中的一个案例进行说明。为了讲述清楚状态机逻辑,在细节上做了简化处理,但丝毫不影响对过程的理解。打一通电话可以被分为如下几个过程:拨打 振铃 接通 (呼叫保持) 挂断 回到拨打界面,打括号的过程也可以没有。依据这几个过程,可以将一通电话的流程划分为如下几个状态:
- calling(拨打中)
- alerting(振铃中)
- called(接通中)
- hold(呼叫保持中)
- disconnect(挂断中)
- idle(拨打界面)
为了方便处理异常,这里有两种思路,一种是定义异常状态,只要流程中出现了不期望的事情,如传参异常、transition的nextStatus为空,当前状态为空,都可以直接进入异常状态,触发异常的处理;第二种是流程都正常,但是传入的EVENT未能满足condition,无法触发状态转移,为了方便统一集中处理这种情况,可以定义一个父状态,在父状态中统一处理。因此,再增加两个状态:
- group(前面六种状态的父状态)
- error_state(异常状态)
状态的转移条件做如下定义,下图将转移流程图做了详细描绘,可以对照着看。
typedef enum
{
CONDITION_CALLING_OUT, /*呼出*/
CONDITION_CALLING_IN, /*呼入*/
CONDITION_ALERTING_OUT, /*对方振铃*/
CONDITION_ALERTING_IN, /*本机振铃*/
CONDITION_CALLED_OUT, /*对方接通*/
CONDITION_CALLED_IN, /*本机接通*/
CONDITION_HOLD_OUT, /*对方呼叫保持*/
CONDITION_HOLD_IN, /*本机呼叫保持*/
CONDITION_RETRIEVE_OUT, /*对方恢复通话*/
CONDITION_RETRIEVE_IN, /*本机恢复保持*/
CONDITION_DISCONNECT_OUT, /*对方挂断*/
CONDITION_DISCONNECT_IN, /*本机挂断*/
CONDITION_CALL_GO_TO_IDLE,/*返回到拨号界面*/
CONDITION_CALL_MAX,
} CONDITION_TYPES_E;
本次实验的代码如下:
#include "stateMachine.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef enum
{
EVENT_CALL_TYPE,
EVENT_CALL_MAX,
} EVENT_TYPES_E;
typedef enum
{
CONDITION_CALLING_OUT,
CONDITION_CALLING_IN,
CONDITION_ALERTING_OUT,
CONDITION_ALERTING_IN,
CONDITION_CALLED_OUT,
CONDITION_CALLED_IN,
CONDITION_HOLD_OUT,
CONDITION_HOLD_IN,
CONDITION_RETRIEVE_OUT,
CONDITION_RETRIEVE_IN,
CONDITION_DISCONNECT_OUT,
CONDITION_DISCONNECT_IN,
CONDITION_CALL_GO_TO_IDLE,
CONDITION_CALL_MAX,
} CONDITION_TYPES_E;
typedef struct
{
CONDITION_TYPES_E condition;
} CONDITION_T;
typedef struct
{
CONDITION_TYPES_E condition;
const char *expected_state;
} EVENT_PAY_LOAD_T;
static void entryAction( void *stateData, struct event *event );
static void exitAction( void *stateData, struct event *event );
static void transAction( void *oldStateData, struct event *event,
void *newStateData );
static bool guard( void *condition, struct event *event );
static void errorEventInput( void *oldStateData, struct event *event,
void *newStateData );
static struct state group_state, calling, alerting, called, hold, disconnect, idle, error_state;
static struct state
group_state =
{
.data="Group",
.entryAction = &entryAction,
.exitAction = &exitAction,
.transitions = (struct transition[])
{
{EVENT_CALL_TYPE, NULL, NULL, &errorEventInput, &idle},
},
.numTransitions = 1,
.parentState = NULL,
.entryState = &idle,
},
idle =
{
.data = "Idle",
.entryAction = &entryAction,
.exitAction = &exitAction,
.transitions = (struct transition[])
{
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_CALLING_OUT}, &guard, &transAction, &calling},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_CALLING_IN}, &guard, &transAction, &calling},
},
.numTransitions = 2,
.parentState = &group_state,
.entryState = NULL,
},
calling =
{
.data = "Calling",
.entryAction = &entryAction,
.exitAction = &exitAction,
.transitions = (struct transition[])
{
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_ALERTING_OUT}, &guard, &transAction, &alerting},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_ALERTING_IN}, &guard, &transAction, &alerting},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_OUT}, &guard, &transAction, &disconnect},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_IN}, &guard, &transAction, &disconnect},
},
.numTransitions = 4,
.parentState = &group_state,
.entryState = NULL,
},
alerting =
{
.data = "Alerting",
.entryAction = &entryAction,
.exitAction = &exitAction,
.transitions = (struct transition[])
{
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_CALLED_OUT}, &guard, &transAction, &called},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_CALLED_IN}, &guard, &transAction, &called},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_OUT}, &guard, &transAction, &disconnect},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_IN}, &guard, &transAction, &disconnect},
},
.numTransitions = 4,
.parentState = &group_state,
.entryState = NULL,
},
called =
{
.data = "Called",
.entryAction = &entryAction,
.exitAction = &exitAction,
.transitions = (struct transition[])
{
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_HOLD_OUT}, &guard, &transAction, &hold},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_HOLD_IN}, &guard, &transAction, &hold},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_OUT}, &guard, &transAction, &disconnect},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_IN}, &guard, &transAction, &disconnect},
},
.numTransitions = 4,
.parentState = &group_state,
.entryState = NULL,
},
hold =
{
.data = "Hold",
.entryAction = &entryAction,
.exitAction = &exitAction,
.transitions = (struct transition[])
{
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_RETRIEVE_OUT}, &guard, &transAction, &called},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_RETRIEVE_IN}, &guard, &transAction, &called},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_OUT}, &guard, &transAction, &disconnect},
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_DISCONNECT_IN}, &guard, &transAction, &disconnect},
},
.numTransitions = 4,
.parentState = &group_state,
.entryState = NULL,
},
disconnect =
{
.data = "Disconnect",
.entryAction = &entryAction,
.exitAction = &exitAction,
.transitions = (struct transition[])
{
{EVENT_CALL_TYPE, &(CONDITION_T){CONDITION_CALL_GO_TO_IDLE}, &guard, &transAction, &idle},
},
.numTransitions = 1,
.parentState = &group_state,
.entryState = NULL,
},
error_state =
{
.data = "ERROR",
.entryAction = &entryAction,
.parentState = &group_state,
.entryState = NULL,
};
int main()
{
struct stateMachine fsm;
stateM_init( &fsm, &idle, &error_state);
struct event events[] = {
/* Create transitions, with the single character as triggering event
* data, and the expected new state name as the following string. '*' is
* used when the unconditional transition will be followed. */
{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ CONDITION_CALLING_OUT, "Calling"} },
{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ CONDITION_ALERTING_OUT, "Alerting" } },
{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ CONDITION_CALLED_OUT, "Called" } },
{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ CONDITION_HOLD_OUT, "Hold" } },
{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ CONDITION_RETRIEVE_OUT, "Called" } },
/*{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ 17, "Called" } }, 异常测试时,请打开此注释*/
{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ CONDITION_DISCONNECT_OUT, "Disconnect" } },
{ EVENT_CALL_TYPE, &(EVENT_PAY_LOAD_T){ CONDITION_CALL_GO_TO_IDLE, "Idle" } },
};
int res;
size_t i;
/* Hand all but the last event to the state machine: */
for ( i = 0; i < sizeof( events ) / sizeof( events[ 0 ] ); ++i )
{
res = stateM_handleEvent( &fsm, &events[ i ] );
if ( res == stateM_stateLoopSelf )
{
/* Prevent segmentation faults (due to the following comparison)
* (loops will not be tested in the first transition): */
if ( i == 0 )
{
fputs( "Internal error. This should not happen.\n", stderr );
exit( 4 );
}
/* Ensure that the reported state loop is indeed a state loop (check
* that the expected state is the same as the previous expected
* state): */
if ( !strcmp( ( (EVENT_PAY_LOAD_T *)events[ i ].data
)->expected_state, ((EVENT_PAY_LOAD_T *)events[ i - 1 ]
.data )->expected_state ) )
puts( "State changed back to itself" );
else
{
fputs( "State unexpectedly changed back to itself", stderr );
exit( 5 );
}
}
/* Apart from an occasional state loop, all other events handed to the
* state machine should result in 'stateM_stateChanged': */
else if ( res != stateM_stateChanged )
{
fprintf( stderr, "Unexpected return value from stateM_handleEvent:"
" %d\n", res );
exit( 2 );
}
}
return res;
}
static void entryAction( void *stateData, struct event *event )
{
printf( "Entering %s\n", (char *)stateData );
}
static void exitAction( void *stateData, struct event *event )
{
printf( "Exiting %s\n", (char *)stateData );
}
static void transAction( void *oldStateData, struct event *event,
void *newStateData )
{
EVENT_PAY_LOAD_T *eventData = (EVENT_PAY_LOAD_T *)event->data;
printf( "Event %s\n", eventData->expected_state );
if ( strcmp( ( (const char *)newStateData ), eventData->expected_state ) )
{
fprintf( stderr, "Unexpected state transition (to %s)\n",
(const char *)newStateData );
exit( 1 );
}
}
static void errorEventInput( void *oldStateData, struct event *event,
void *newStateData )
{
EVENT_PAY_LOAD_T *eventData = (EVENT_PAY_LOAD_T *)event->data;
printf( "Error Event condition=%d currentState=%s nextState=%s\n",
eventData->condition,(char *)oldStateData,(char *)newStateData);
return;
}
static bool guard( void *condition, struct event *event )
{
EVENT_PAY_LOAD_T *eventData = (EVENT_PAY_LOAD_T *)event->data;
return ((CONDITION_T *)condition)->condition == eventData->condition;
}
下面第一张图片,是正常的测试流程,事件流程为:
- CONDITION_CALLING_OUT
- CONDITION_ALERTING_OUT
- CONDITION_CALLED_OUT
- CONDITION_HOLD_OUT
- CONDITION_RETRIEVE_OUT
- CONDITION_DISCONNECT_OUT
- CONDITION_CALL_GO_TO_IDLE
从图片log中,可以看到exitAction(),transitions action(),entryAction()随着事件,交错执行,符合状态转移过程。
下面第二张图片,事件流程为:
- CONDITION_CALLING_OUT
- CONDITION_ALERTING_OUT
- CONDITION_CALLED_OUT
- CONDITION_HOLD_OUT
- CONDITION_RETRIEVE_OUT
- 17
- CONDITION_DISCONNECT_OUT
- CONDITION_CALL_GO_TO_IDLE
唯一和上面正常的不同是在第六点增加了事件流程17,由于当前状态没有处理此事件的transition,此事件被转移到了父状态的transition来处理。在父状态的transitions action()中,打出了如下的异常log:Error Event condition=10 currentState=Idle nextState=Idle。
4 注意事项
- 可以利用父状态和异常状态来处理异常行为;
- 在父状态的子状态中,只有一个子状态可以充当entryState。如本例中group有很多子状态,但只有idle充当其entryState;
- 如果将要转移的状态有entryState,将要转移的状态会变为entryState本身,并递归的往下进行;
- 每个状态和transition对应的
exitAction(),transitions action(),entryAction()回调函数可以自住定义,都可以不一样。本例为了简化,回调函数都复用了。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。