介绍一个状态机框架,纯c语言打造(附源码)

8,732 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

"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 eventstruct transitionstruct statestruct 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()的调用流程大概可以描述如下:

  1. 判断入参fsmevent是否为空,为空时直接返回stateM_errArg
  2. 判断当前状态是否为空,如果为空,将当前状态置为errorState,返回stateM_errorStateReached
  3. 判断当前状态的转移数组numTransitions大小是否为零,如果为零,返回stateM_noStateChange;
  4. 获取当前状态的transition,如果当前状态没有transition,则获取父状态的transition,如果往上遍历父状态transition都没有,直到父状态为空,返回stateM_noStateChange
  5. 判断步骤(4.)拿到的transition,是否有nextState,如果为空,将当前状态置为errorState,返回stateM_errorStateReached
  6. 拿到步骤(5.)中的nextState,向下遍历nextStateentryState,直到nextState没有entryState,然后将此刻的nextState,作为将要转移过去的状态;
  7. 如果当前状态和将要转移过去的状态不相等,则回调exitAction()函数;
  8. 回调transitionaction()函数;
  9. 如果当前状态和将要转移过去的状态不相等,则回调entryAction()函数;
  10. 如果当前状态和将要转移过去的状态相等,返回stateM_stateLoopSelf
  11. 如果当前状态等于errorState,则返回stateM_errorStateReached
  12. 如果将要转移状态的转移数组numTransitions大小为零,则返回stateM_finalStateReached
  13. 如果上面都没有遇到异常,则流程执行完成,返回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 应用实例

本次实例用实际开发过程中的一个案例进行说明。为了讲述清楚状态机逻辑,在细节上做了简化处理,但丝毫不影响对过程的理解。打一通电话可以被分为如下几个过程:拨打 \rightarrow 振铃 \rightarrow 接通 \rightarrow (呼叫保持) \rightarrow 挂断 \rightarrow 回到拨打界面,打括号的过程也可以没有。依据这几个过程,可以将一通电话的流程划分为如下几个状态:

  • 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;
}

下面第一张图片,是正常的测试流程,事件流程为:

  1. CONDITION_CALLING_OUT
  2. CONDITION_ALERTING_OUT
  3. CONDITION_CALLED_OUT
  4. CONDITION_HOLD_OUT
  5. CONDITION_RETRIEVE_OUT
  6. CONDITION_DISCONNECT_OUT
  7. CONDITION_CALL_GO_TO_IDLE

从图片log中,可以看到exitAction()transitions action()entryAction()随着事件,交错执行,符合状态转移过程。

下面第二张图片,事件流程为:

  1. CONDITION_CALLING_OUT
  2. CONDITION_ALERTING_OUT
  3. CONDITION_CALLED_OUT
  4. CONDITION_HOLD_OUT
  5. CONDITION_RETRIEVE_OUT
  6. 17
  7. CONDITION_DISCONNECT_OUT
  8. 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()回调函数可以自住定义,都可以不一样。本例为了简化,回调函数都复用了。

我正在参与掘金技术社区创作者签约计划招募活动点击链接报名投稿