Conception de la machine à états C [fermé]


193

Je crée un petit projet en C et C ++ mixtes. Je construis une petite machine d'état au cœur d'un de mes fils de travail.

Je me demandais si vous, gourous du SO, partageriez vos techniques de conception de machines à états.

REMARQUE: je recherche principalement des techniques d'implémentation éprouvées.

MISE À JOUR: Sur la base de toutes les grandes contributions recueillies sur SO, je me suis installé sur cette architecture:

Une pompe d'événement pointe vers un intégrateur d'événement qui pointe vers un répartiteur.  Le répartiteur pointe sur 1 à n actions qui pointent vers l'intégrateur d'événements.  Une table de transition avec des caractères génériques pointe vers le répartiteur.


4
Les réponses ici sont très bonnes. Regardez également cette question en double qui a également plusieurs bonnes réponses: stackoverflow.com/questions/1371460/state-machines-tutorials
Michael Burr



Réponses:


170

Les machines à états que j'ai conçues auparavant (C, pas C ++) se résument toutes à un structtableau et à une boucle. La structure se compose essentiellement d'un état et d'un événement (pour la recherche) et d'une fonction qui renvoie le nouvel état, quelque chose comme:

typedef struct {
    int st;
    int ev;
    int (*fn)(void);
} tTransition;

Ensuite, vous définissez vos états et événements avec des définitions simples ( ANYcelles-ci sont des marqueurs spéciaux, voir ci-dessous):

#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

Ensuite, vous définissez toutes les fonctions appelées par les transitions:

static int GotKey (void) { ... };
static int FsmError (void) { ... };

Toutes ces fonctions sont écrites pour ne prendre aucune variable et retourner le nouvel état de la machine d'état. Dans cet exemple, des variables globales sont utilisées pour transmettre toute information dans les fonctions d'état lorsque cela est nécessaire.

L'utilisation de globaux n'est pas aussi mauvaise qu'il y paraît car le FSM est généralement enfermé dans une seule unité de compilation et toutes les variables sont statiques à cette unité (c'est pourquoi j'ai utilisé des guillemets autour de "global" ci-dessus - ils sont plus partagés dans le FSM, que vraiment mondiale). Comme pour tous les pays du monde, cela nécessite des soins.

Le tableau de transitions définit ensuite toutes les transitions possibles et les fonctions qui sont appelées pour ces transitions (y compris la dernière fourre-tout):

tTransition trans[] = {
    { ST_INIT, EV_KEYPRESS, &GotKey},
    : :
    { ST_ANY, EV_ANY, &FsmError}
};
#define TRANS_COUNT (sizeof(trans)/sizeof(*trans))

Cela signifie: si vous êtes dans l' ST_INITétat et que vous recevez l' EV_KEYPRESSévénement, appelez GotKey.

Le fonctionnement du FSM devient alors une boucle relativement simple:

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

Comme mentionné ci-dessus, notez l'utilisation de caractères ST_ANYgénériques, permettant à un événement d'appeler une fonction quel que soit l'état actuel. EV_ANYfonctionne également de façon similaire, permettant à tout événement à un état spécifique d'appeler une fonction.

Il peut également garantir que si vous atteignez la fin du tableau de transitions, vous obtenez une erreur indiquant que votre FSM n'a pas été construit correctement (en utilisant la ST_ANY/EV_ANYcombinaison.

J'ai utilisé un code similaire pour cela sur de nombreux projets de communication, comme une mise en œuvre précoce de piles de communications et de protocoles pour les systèmes embarqués. Le gros avantage était sa simplicité et sa relative facilité à changer le tableau de transitions.

Je ne doute pas qu'il y aura des abstractions de niveau supérieur qui peuvent être plus adaptées de nos jours, mais je pense qu'elles se résumeront toutes à ce même type de structure.


Et, comme l' ldogindique un commentaire, vous pouvez éviter les globaux en passant un pointeur de structure à toutes les fonctions (et en l'utilisant dans la boucle d'événement). Cela permettra à plusieurs machines d'état de fonctionner côte à côte sans interférence.

Créez simplement un type de structure qui contient les données spécifiques à la machine (état au strict minimum) et utilisez-les à la place des globales.

La raison pour laquelle je l'ai rarement fait est simplement parce que la plupart des machines d'état que j'ai écrites sont de type singleton (one-off, at-process-start, lecture de fichier de configuration par exemple), n'ayant pas besoin d'exécuter plus d'une instance . Mais il a de la valeur si vous devez en exécuter plusieurs.


24
Un commutateur géant mélange le code avec le FSM. Même s'il n'y a qu'un appel de fonction par transition, il y a encore du code, et il est facile pour quelqu'un d'en abuser en ajoutant simplement une petite transition de 4 lignes en ligne. poule une ligne de dix. Ensuite, cela devient incontrôlable. Avec le tableau struct, le FSM reste propre - vous pouvez voir chaque transition et l'effet (fonction). Et j'ai commencé quand les énumérations étaient un clin d'œil à l'ISO, écrivant du code pour les plates-formes embarquées 6809 avec des compilateurs qui étaient, disons, moins que parfaits :-)
paxdiablo

5
Vous avez raison, les énumérations seraient mieux, mais je préfère toujours avoir le FSM en tant que tableau struct. Ensuite, tout est géré par des données plutôt que par du code (eh bien, il y a du code mais les chances de bourrer la boucle FSM que j'ai donnée sont minces).
paxdiablo

2
C'est bien, pour les machines à états contrôlées par processus, j'avais l'habitude d'ajouter toujours trois (éventuellement vides) sous-états pour chaque état, de sorte que l'appel à une fonction d'état deviendrait GotKey (sous-état), où sous-état serait: - SS_ENTRY - SS_RUN - SS_EXIT Fondamentalement, la fonction d'état est appelée avec un sous-état SS_ENTRY à l'entrée, afin que l'état puisse reconstruire un état (par exemple, les positions des actionneurs). Bien qu'il n'y ait pas de transition, la valeur de sous-état SS_RUN est transmise. Lors de la transition, la fonction d'état est appelée avec le sous-état SS_EXIT, afin qu'elle puisse effectuer tous les nettoyages (par exemple, désallouer les ressources).
Metiu

13
Vous avez mentionné que vous partagez des données à l'aide de globaux, mais il serait probablement plus propre si vous définissez les fonctions d'état pour être int (*fn)(void*);où se void*trouve le pointeur vers les données que chaque fonction d'état prend en paramètre. Ensuite, les fonctions d'état peuvent soit utiliser les données, soit les ignorer.
ldog

13
J'utilise la même séparation données / code pour écrire des FSM, sauf qu'il ne m'est jamais venu à l'esprit d'introduire des états «génériques». Idée intéressante! Cependant, itérer le tableau de transitions peut devenir coûteux si vous avez beaucoup d'états (ce qui était le cas pour moi puisque le code C a été généré automatiquement). Dans de telles situations, il serait plus efficace d'avoir un tableau de transitions par état. Un état n'est donc plus une valeur énumérée, mais une table de transition. De cette façon, vous n'avez pas à répéter toutes les transitions de la machine, mais seulement celles qui sont pertinentes pour l'état actuel.
Frerich Raabe

78

Les autres réponses sont bonnes, mais une implémentation très "légère" que j'ai utilisée lorsque la machine d'état est très simple ressemble à:

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 ... */
    }
}

J'utiliserais cela lorsque la machine à états est suffisamment simple pour que l'approche du pointeur de fonction et de la table de transition d'état soit exagérée. Ceci est souvent utile pour l'analyse syntaxique caractère par caractère ou mot par mot.


37

Excusez-moi d'avoir enfreint toutes les règles de l'informatique, mais une machine d'état est l'un des rares endroits (je ne peux en compter que deux à la main) où une gotodéclaration est non seulement plus efficace, mais rend également votre code plus propre et plus facile à lire. Étant donné que les gotoinstructions sont basées sur des étiquettes, vous pouvez nommer vos états au lieu d'avoir à suivre un désordre de nombres ou à utiliser une énumération. Cela rend également le code beaucoup plus propre, car vous n'avez pas besoin de toute la cruauté supplémentaire des pointeurs de fonction ou des énormes instructions de commutateur et des boucles while. Ai-je mentionné que c'est aussi plus efficace?

Voici à quoi pourrait ressembler une machine d'état:

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

Vous avez l'idée générale. Le fait est que vous pouvez implémenter la machine d'état d'une manière efficace et qui est relativement facile à lire et qui crie au lecteur qu'il regarde une machine d'état. Notez que si vous utilisez des gotoinstructions, vous devez toujours être prudent car il est très facile de se tirer une balle dans le pied.


4
Cela ne fonctionne que si la machine d'état se trouve dans l'objet de niveau supérieur. Au moment où un autre objet qui est parfois interrogé / envoyé des messages, doit avoir un état, vous êtes coincé avec cette approche (cela, ou vous devez le rendre beaucoup plus compliqué)
skrebbel

1
Cela vous oblige vraiment à utiliser le multitâche préemptif dans tous les cas, sauf le plus simple.
Craig McQueen

1
Ces gotos pourraient être remplacés par des appels de fonction. Et si un profileur vous dit que votre programme se noie à cause de la surcharge des appels de fonction, vous pouvez remplacer les appels par des gotos si nécessaire.
Abtin Forouzandeh, le

7
@AbtinForouzandeh simplement remplacer les gotos par des appels de fonction provoquerait un débordement de pile puisque la pile d'appels n'est effacée qu'en cas d'erreur.
JustMaximumPower

Je suis d'accord avec la méthode goto. Voici un ensemble de macros qui illustrent cela. Et les macros structurent votre code comme si vous l'aviez codé comme vous le feriez normalement. Il travaille également au niveau d' interruption qui est habituellement où les machines d'état sont nécessaires codeproject.com/Articles/37037/...
eddyq

30

Vous pourriez envisager le compilateur State Machine http://smc.sourceforge.net/

Ce splendide utilitaire open source accepte la description d'une machine à états dans un langage simple et la compile dans n'importe laquelle d'une douzaine de langages - y compris C et C ++. L'utilitaire lui-même est écrit en Java et peut être inclus dans le cadre d'une build.

La raison de cela, plutôt que de coder manuellement à l'aide du modèle d'état GoF ou de toute autre approche, est qu'une fois que votre machine d'état est exprimée sous forme de code, la structure sous-jacente a tendance à disparaître sous le poids du passe-partout qui doit être généré pour le prendre en charge. L'utilisation de cette approche vous offre une excellente séparation des préoccupations et vous gardez la structure de votre machine d'état «visible». Le code généré automatiquement va dans des modules que vous n'avez pas besoin de toucher, de sorte que vous pouvez revenir en arrière et jouer avec la structure de la machine d'état sans impact sur le code de support que vous avez écrit.

Désolé, je suis trop enthousiaste, et sans doute rebute tout le monde. Mais c'est un utilitaire de premier ordre, et bien documenté aussi.


20

N'oubliez pas de vérifier le travail de Miro Samek (blog State Space , site Web State Machines & Tools ), dont les articles du C / C ++ Users Journal étaient excellents.

Le site Web contient une implémentation complète (C / C ++) en licence open source et commerciale d'une infrastructure de machine à états (QP Framework) , d'un gestionnaire d'événements (QEP) , d'un outil de modélisation de base (QM) et d'un outil de traçage (QSpy) qui permettent de dessiner des machines à états, de créer du code et de les déboguer.

Le livre contient une explication détaillée sur le quoi / pourquoi de la mise en œuvre et comment l'utiliser et est également un excellent matériel pour acquérir une compréhension des principes fondamentaux des machines à états hiérarchiques et finis.

Le site Web contient également des liens vers plusieurs packages de support de carte pour l'utilisation du logiciel avec des plates-formes intégrées.


J'ai modifié le titre de la question en fonction de votre jeu de mots.
jldupont

@jldupont: Je voulais juste dire qu'il valait mieux clarifier. J'ai supprimé les parties non pertinentes de ma réponse maintenant.
Daniel Daranas,

1
J'ai ajouté à quoi m'attendre sur le site Web / livre, après avoir utilisé le logiciel avec succès; c'est le meilleur livre de ma bibliothèque.
Adriaan

@Adriann, grande explication! Je viens de modifier la page d'accueil du site Web, le lien précédent avait cessé de fonctionner.
Daniel Daranas

2
Les liens sont morts ou pointent vers la page d'accueil du site qui semble avoir changé son orientation vers les logiciels embarqués. Vous pouvez toujours voir une partie du contenu sur state-machine.com/resources/articles.php , mais même là, la plupart des liens liés à la machine d'état sont morts. C'est l'un des seuls bons liens là-bas: state-machine.com/resources/…
Tatiana Racheva

11

J'ai fait quelque chose de similaire à ce que décrit paxdiablo, mais au lieu d'un tableau de transitions état / événement, j'ai mis en place un tableau bidimensionnel de pointeurs de fonction, avec la valeur d'événement comme index d'un axe et la valeur d'état actuelle comme L'autre. Ensuite, j'appelle state = state_table[event][state](params)et la bonne chose se produit. Les cellules représentant des combinaisons état / événement invalides obtiennent un pointeur sur une fonction qui le dit, bien sûr.

Évidemment, cela ne fonctionne que si les valeurs d'état et d'événement sont toutes les deux des plages contiguës et commencent à 0 ou assez près.


1
On dirait que cette solution ne s'adapte pas bien: trop de remplissage de table, non?
jldupont

2
+1. Le problème de mise à l'échelle ici est la mémoire - ma propre solution a un problème de mise à l'échelle concernant le temps, c'est-à-dire le temps pris pour analyser la table des transitions (bien que vous puissiez optimiser manuellement pour les transitions les plus courantes). Celui-ci sacrifie la mémoire pour la vitesse - c'est juste un compromis. Vous auriez probablement besoin de vérifier les limites, mais ce n'est pas une mauvaise solution.
paxdiablo

Les gars - Mon commentaire n'est pas sorti comme prévu: je voulais dire qu'il est beaucoup plus laborieux et sujet aux erreurs. Si vous ajoutez un état / événement, de nombreuses modifications doivent être effectuées.
jldupont

3
Personne n'a dit que le tableau 2D avait été initialisé à la main. Peut-être qu'il y a quelque chose qui lit un fichier de configuration et le crée (ou du moins il pourrait certainement y en avoir).
John Zwinck

Une façon d'initialiser des tableaux de ce type consiste à utiliser le préprocesseur comme un classeur tardif (par opposition à une liaison anticipée). Vous définissez une liste de tous les états #define STATE_LIST() \STATE_LIST_ENTRY(state1)\STATE_LIST_ENTRY(state2)\...(une nouvelle ligne implicite après chaque \ ) où vous (re) définissez la macro d'entrée lorsque vous utilisez la macro STATE_LIST. Exemple - faire un tableau de noms d'état: #define STATE_LIST_ENTRY(s) #s , \n const char *state_names[] = { STATE_LIST() };\n #undef STATE_LIST_ENTRY. Certains travaux doivent être effectués en premier, mais c'est extrêmement puissant. Ajouter un nouvel état -> garanti aucun échec.
hlovdal

9

Un très joli "framework" de machine d'état C ++ basé sur un modèle est donné par Stefan Heinzmann dans son article .

Puisqu'il n'y a pas de lien vers un téléchargement de code complet dans l'article, j'ai pris la liberté de coller le code dans un projet et de le vérifier. Le matériel ci-dessous est testé et comprend les quelques pièces manquantes mineures mais à peu près évidentes.

L'innovation majeure ici est que le compilateur génère du code très efficace. Les actions d'entrée / sortie vides sont gratuites. Les actions d'entrée / sortie non vides sont intégrées. Le compilateur vérifie également l'exhaustivité du diagramme d'états. Les actions manquantes génèrent des erreurs de liaison. La seule chose qui n'est pas capturée, ce sont les disparusTop::init .

C'est une très belle alternative à l'implémentation de Miro Samek, si vous pouvez vivre sans ce qui manque - c'est loin d'être une implémentation UML Statechart complète, bien qu'elle implémente correctement la sémantique UML, alors que le code de Samek par conception ne gère pas la sortie / transition / actions de saisie dans l'ordre correct.

Si ce code fonctionne pour ce que vous devez faire et que vous avez un compilateur C ++ décent pour votre système, il fonctionnera probablement mieux que l'implémentation C / C ++ de Miro. Le compilateur génère pour vous une implémentation de la machine à états de transition O (1) aplatie. Si l'audit des sorties d'assemblage confirme que les optimisations fonctionnent comme vous le souhaitez, vous vous rapprochez des performances théoriques. La meilleure partie: c'est un code relativement petit et facile à comprendre.

#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

Le code de test suit.

#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)

Hmm ... il manque quelque chose dans votre code. Tout d'abord, vous incluez deux en-têtes, mais ne fournissez que le premier. Lorsque je commente simplement l'instruction "include", j'obtiens cette erreur lors de la compilation: d: \ 1 \ hsm> g ++ test.cpp test.cpp: 195: 1: erreur: spécialisation de 'static void CompState <H, id, B> :: init (H &) [avec H = TestHSM; unsigned int id = 0u; B = CompState <TestHSM, 0u, TopState <TestHSM>>] 'après instanciation
Freddie Chopin

J'ai dû déplacer les définitions de tous les HSMINIT () pour être au-dessus de la classe TestHSM et cela compile et fonctionne bien (; La seule chose qui ne va pas est le fait que toutes les transitions sont "externes", alors qu'elles devraient être "internes" - il y avait un débat à ce sujet dans l'article et l'auteur a décidé que "extrenal" avait raison, mais les flèches utilisées suggèrent "interne".
Freddie Chopin

5

La technique que j'aime pour les machines à états (au moins celles pour le contrôle de programme) est d'utiliser des pointeurs de fonction. Chaque état est représenté par une fonction différente. La fonction prend un symbole d'entrée et renvoie le pointeur de fonction pour l'état suivant. Le moniteur de boucle de répartition centrale prend l'entrée suivante, la transmet à l'état actuel et traite le résultat.

Le fait de taper dessus devient un peu étrange, car C n'a pas de moyen d'indiquer les types de pointeurs de fonction qui se retournent, donc les fonctions d'état reviennent void*. Mais vous pouvez faire quelque chose comme ça:

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

Ensuite, vos fonctions d'état individuelles peuvent activer leur entrée pour traiter et renvoyer la valeur appropriée.


+1 c'est vraiment sympa, et offre de belles fonctionnalités à portée de main dans les fonctions de transition
Fire Crow

5

Cas le plus simple

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

Points: l'état est privé, non seulement pour l'unité de compilation mais aussi pour le gestionnaire d'événements. Les cas spéciaux peuvent être traités séparément de l'interrupteur principal en utilisant la construction jugée nécessaire.

Cas plus complexe

Lorsque le commutateur est plus grand que deux écrans pleins, divisez-le en fonctions qui gèrent chaque état, en utilisant une table d'états pour rechercher directement la fonction. L'État est toujours privé du gestionnaire d'événements. Les fonctions du gestionnaire d'état renvoient l'état suivant. Si nécessaire, certains événements peuvent toujours recevoir un traitement spécial dans le gestionnaire d'événements principal. J'aime lancer des pseudo-événements pour l'entrée et la sortie d'état et peut-être le démarrage de la machine d'état:

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

Je ne sais pas si j'ai cloué la syntaxe, en particulier en ce qui concerne le tableau des pointeurs de fonction. Je n'ai exécuté rien de tout cela via un compilateur. Après examen, j'ai remarqué que j'avais oublié de supprimer explicitement l'état suivant lors de la gestion des pseudo-événements (la parenthèse (void) avant l'appel à state_handler ()). C'est quelque chose que j'aime faire même si les compilateurs acceptent l'omission en silence. Il indique aux lecteurs du code que "oui, je voulais en effet appeler la fonction sans utiliser la valeur de retour", et cela peut empêcher les outils d'analyse statique de l'avertir. C'est peut-être idiosyncratique parce que je ne me souviens pas avoir vu quelqu'un d'autre faire cela.

Points: ajouter un tout petit peu de complexité (vérifier si l'état suivant est différent de l'état actuel), peut éviter le code dupliqué ailleurs, car les fonctions du gestionnaire d'état peuvent profiter des pseudo-événements qui se produisent lorsqu'un état est entré et quitté. N'oubliez pas que l'état ne peut pas changer lors de la gestion des pseudo-événements, car le résultat du gestionnaire d'état est ignoré après ces événements. Vous pouvez bien sûr choisir de modifier le comportement.

Un gestionnaire d'état ressemblerait à ceci:

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

Plus de complexité

Lorsque l'unité de compilation devient trop grande (quelle que soit votre impression, je devrais dire environ 1 000 lignes), placez chaque gestionnaire d'état dans un fichier distinct. Lorsque chaque gestionnaire d'état devient plus long que quelques écrans, divisez chaque événement dans une fonction distincte, similaire à la façon dont le commutateur d'état a été divisé. Vous pouvez le faire de différentes manières, indépendamment de l'état ou en utilisant une table commune ou en combinant différents schémas. Certains d'entre eux ont été couverts ici par d'autres. Triez vos tableaux et utilisez la recherche binaire si la vitesse est une exigence.

Programmation générique

Je voudrais que le préprocesseur traite des problèmes tels que le tri des tables ou même la génération de machines à états à partir de descriptions, vous permettant "d'écrire des programmes sur des programmes". Je crois que c'est pour cela que les gens de Boost exploitent les modèles C ++, mais je trouve la syntaxe cryptique.

Tables bidimensionnelles

J'ai utilisé des tables d'état / d'événements dans le passé, mais je dois dire que pour les cas les plus simples, je ne les trouve pas nécessaires et je préfère la clarté et la lisibilité de l'instruction switch même si elle s'étend au-delà d'un écran complet. Pour les cas plus complexes, les tableaux deviennent rapidement incontrôlables comme d'autres l'ont noté. Les idiomes que je présente ici vous permettent d'ajouter une multitude d'événements et d'états quand vous en avez envie, sans avoir à maintenir une table consommatrice de mémoire (même s'il peut s'agir de mémoire de programme).

Avertissement

Des besoins spéciaux peuvent rendre ces idiomes moins utiles, mais je les ai trouvés très clairs et maintenables.


J'éviterais «ceci» comme nom ou symbole de variable juste pour l'association, même si ce n'est pas réellement un mot réservé.
XTL

4

Extrêmement non testé, mais amusant à coder, maintenant dans une version plus raffinée que ma réponse originale; des versions à jour peuvent être trouvées sur 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

exemple.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;
}

14
J'adore le commentaire "extrêmement non testé". Semble indiquer qu'il y a des degrés de non-test et que vous mettez beaucoup d'efforts pour ne pas le tester :-)
paxdiablo

@Christoph le lien dans cette réponse est rompu. De plus, avez-vous testé ce code ou non? S'il a été testé et fonctionne, vous devez le supprimer de la réponse. Peut-être aussi montrer un exemple de quel code cela résulte une fois que les macros ont été développées. J'aime l'idée générale.
Joakim

4

J'ai vraiment aimé la réponse de paxdiable et j'ai décidé d'implémenter toutes les fonctionnalités manquantes pour mon application comme les variables de garde et les données spécifiques à la machine d'état.

J'ai téléchargé mon implémentation sur ce site pour la partager avec la communauté. Il a été testé à l'aide de IAR Embedded Workbench for ARM.

https://sourceforge.net/projects/compactfsm/


Trouver cela en 2018 et qu'il est toujours applicable. Je lisais @paxdiablo answer, et j'ai déjà utilisé avec succès ce type d'implémentation dans des systèmes embarqués. Cette solution ajoute les éléments manquants de la réponse de paxdiablos :)
Kristoffer

4

Un autre outil open source intéressant est Yakindu Statechart Tools sur statecharts.org . Il utilise les diagrammes d'état Harel et fournit ainsi des états hiérarchiques et parallèles et génère du code C et C ++ (ainsi que Java). Il n'utilise pas de bibliothèques mais suit une approche de «code simple». Le code applique essentiellement des structures de cas de commutation. Les générateurs de code peuvent également être personnalisés. De plus, l'outil offre de nombreuses autres fonctionnalités.


3

J'arrive tard (comme d'habitude) mais en parcourant les réponses à ce jour, je pense qu'il manque quelque chose d'important;

J'ai trouvé dans mes propres projets qu'il peut être très utile de ne pas avoir de fonction pour chaque combinaison état / événement valide. J'aime l'idée d'avoir effectivement une table 2D d'états / événements. Mais j'aime que les éléments du tableau soient plus qu'un simple pointeur de fonction. Au lieu de cela, j'essaie d'organiser ma conception, donc en son cœur, elle comprend un tas d'éléments ou d'actions atomiques simples. De cette façon, je peux lister ces éléments atomiques simples à chaque intersection de ma table d'état / événement. L'idée est que vous ne le faites pas à définir une masse de N fonctions au carré (généralement très simples). Pourquoi avoir quelque chose d'aussi sujet aux erreurs, de temps, difficile à écrire, difficile à lire, vous l'appelez?

J'inclus également un nouvel état facultatif et un pointeur de fonction facultatif pour chaque cellule du tableau. Le pointeur de fonction est là pour les cas exceptionnels où vous ne voulez pas simplement déclencher une liste d'actions atomiques.

Vous savez que vous le faites correctement lorsque vous pouvez exprimer de nombreuses fonctionnalités différentes, simplement en modifiant votre table, sans nouveau code à écrire.


2
Peut-être qu'un exemple serait bien, non?
jldupont

1
Un exemple réaliste qui peut être présenté isolément est une tâche difficile qui nécessiterait plus de temps que je ne suis prêt à en donner pour le moment. Y a-t-il quelque chose dans mon message qui est particulièrement difficile à comprendre? Je peux peut-être l'exprimer plus clairement. L'idée est très simple; Ne définissez pas un mécanisme d'état qui nécessite une fonction distincte pour chaque combinaison événement / état, vous obtenez trop de fonctions de cette façon. Au lieu de cela, trouvez une autre façon de décrire la fonctionnalité que vous souhaitez pour cette combinaison événement / état, au moins dans la majorité des cas.
Bill Forster

2
Compris: un exemple de pseudo-code aurait été bien mais votre point est clair.
jldupont

3

Bon, je pense que le mien est juste un peu différent de tout le monde. Un peu plus de séparation du code et des données que je ne le vois dans les autres réponses. J'ai vraiment lu la théorie pour écrire ceci, qui implémente un langage régulier complet (sans expressions régulières, malheureusement). Ullman, Minsky, Chomsky. Je ne peux pas dire que j'ai tout compris, mais je me suis inspiré des anciens maîtres aussi directement que possible: à travers leurs mots.

J'utilise un pointeur de fonction vers un prédicat qui détermine la transition vers un état «oui» ou un état «non». Cela facilite la création d'un accepteur à états finis pour un langage normal que vous programmez d'une manière plus proche du langage d'assemblage. S'il vous plaît ne soyez pas rebuté par mes choix de noms stupides. 'czek' == 'vérifier'. 'grok' == [allez le chercher dans le dictionnaire Hacker].

Donc, pour chaque itération, czek appelle une fonction de prédicat avec le caractère courant comme argument. Si le prédicat retourne vrai, le caractère est consommé (le pointeur avancé) et nous suivons la transition «y» pour sélectionner l'état suivant. Si le prédicat renvoie faux, le caractère n'est PAS consommé et nous suivons la transition 'n'. Donc, chaque instruction est une branche à double sens! Je devais lire l'histoire de Mel à l'époque.

Ce code vient directement de mon interprète postscript , et a évolué dans sa forme actuelle avec beaucoup de conseils des boursiers sur comp.lang.c. Étant donné que postscript n'a fondamentalement pas de syntaxe (ne nécessitant que des crochets équilibrés), un accepteur de langage régulier comme celui-ci fonctionne également comme analyseur.

/* 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;
    }
}

2
C'est ce que tout générateur d'analyseur ou de lexer émettra volontiers pour vous. Étrangement donc. Que vous souhaitiez le coder à la main est discutable. Il a bien sûr un mérite pédagogique.
Rétablir Monica le

3

boost.org est livré avec 2 implémentations de diagrammes d'état différents:

Comme toujours, le boost vous transportera dans l'enfer des modèles.

La première bibliothèque est destinée aux machines d'état plus critiques pour les performances. La deuxième bibliothèque vous donne un chemin de transition direct d'un diagramme d'états UML au code.

Voici la question SO demandant une comparaison entre les deux où les deux auteurs répondent.



2

J'ai vu ça quelque part

#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);
  }
}

1
C'est intéressant, mais pas de vote positif jusqu'à ce que vous donniez un exemple ou deux (et peut-être un résultat dé-macro) ou une discussion sur pourquoi cela peut être plus pratique qu'un autre. Utilisation intéressante de crochets et de macros orphelins. J'imagine que quelque chose de similaire pourrait être fait sur un langage qui fait une sorte d'optimisation de récursivité de queue; vous pouvez utiliser des appels de fonction directs et ne pas vous soucier de surcharger l'espace de la pile avec des ordures d'appel de fonction (ce qui, je pense, est essentiellement ce que les macros surmontent ici)
Ape-inago

2
Les avantages de cette méthode sont ...? Je vois plusieurs inconvénients, tels que l'obscurcissement des macros, et leur utilisation gotocrée une dépendance à l'égard d'un système d'exploitation multitâche préemptif.
Craig McQueen

2

Étant donné que vous impliquez que vous pouvez utiliser le code C ++ et donc OO, je suggère d'évaluer le modèle d'état `` GoF '' (GoF = Gang of Four, les gars qui ont écrit le livre sur les modèles de conception qui a mis les modèles de conception sous les projecteurs).

Ce n'est pas particulièrement complexe et il est largement utilisé et discuté, il est donc facile de voir des exemples et des explications en ligne.

Il sera également très probablement reconnaissable par toute autre personne conservant votre code à une date ultérieure.

Si l'efficacité est le souci, il vaudrait la peine de faire une analyse comparative pour s'assurer qu'une approche non OO est plus efficace car de nombreux facteurs affectent les performances et ce n'est pas toujours simplement OO mauvais, le code fonctionnel est bon. De même, si l'utilisation de la mémoire est une contrainte pour vous, cela vaut encore la peine de faire des tests ou des calculs pour voir si cela sera réellement un problème pour votre application particulière si vous utilisez le modèle d'état.

Voici quelques liens vers le modèle d'état «Gof», comme le suggère Craig:


ressemble plus à un commentaire: pourrais-je vous suggérer de le traiter comme tel? c'est-à-dire ne pas le placer dans la section "réponse".
jldupont

Ce serait bien si vous pouviez fournir un bon lien URL pour le "modèle d'état GoF", pour ceux qui ne le connaissent pas.
Craig McQueen

1
@jldupont - commentaire juste. J'ai changé le texte pour en faire une bonne réponse car je me sens basé sur l'expérience personnelle que, à moins qu'il y ait des problèmes de performances spécifiques, l'approche du GoF fonctionne bien et aura une `` base d'utilisateurs '' relativement importante
Mick

@Craig - a ajouté quelques liens. Les deux semblaient précis et clairs au moment où je les ai ajoutés.
Mick

2

Voici un exemple de machine à états finis pour Linux qui utilise les files d'attente de messages comme événements. Les événements sont placés dans la file d'attente et traités dans l'ordre. L'état change en fonction de ce qui se passe pour chaque événement.

Ceci est un exemple de connexion de données avec des états tels que:

  • Non initialisé
  • Initialisé
  • Lié
  • MTU négocié
  • Authentifié

Une petite fonctionnalité supplémentaire que j'ai ajoutée était un horodatage pour chaque message / événement. Le gestionnaire d'événements ignorera les événements trop anciens (ils ont expiré). Cela peut se produire beaucoup dans le monde réel où vous pourriez vous retrouver dans un état inattendu.

Cet exemple fonctionne sous Linux, utilisez le Makefile ci-dessous pour le compiler et jouer avec.

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,        &currentlyLessThanMaxConnectTries,      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,            &currentlyLessThanMaxMTUtries,          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,           &currentyLessThanMaxAuthTries,          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,             &currentlyLessThanMaxTXtries,           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

1

Votre question est assez générique,
voici deux articles de référence qui pourraient être utiles,

  1. Implémentation de la machine à états intégrée

    Cet article décrit une approche simple pour implémenter une machine d'état pour un système embarqué. Aux fins du présent article, une machine à états est définie comme un algorithme qui peut se trouver dans l'un d'un petit nombre d'états. Un état est une condition qui provoque une relation prescrite d'entrées à sorties et d'entrées aux états suivants.
    Un lecteur averti remarquera rapidement que les machines d'état décrites dans cet article sont des machines Mealy. Une machine Mealy est une machine d'état où les sorties sont fonction à la fois de l'état actuel et de l'entrée, par opposition à une machine Moore, dans laquelle les sorties sont uniquement fonction de l'état.

    • Codage des machines à états en C et C ++

      Ma préoccupation dans cet article concerne les principes fondamentaux de la machine à états et quelques directives de programmation simples pour coder les machines à états en C ou C ++. J'espère que ces techniques simples peuvent devenir plus courantes, afin que vous (et d'autres) puissiez facilement voir la structure de la machine à états directement depuis le code source.



1

Ceci est un ancien article avec beaucoup de réponses, mais j'ai pensé ajouter ma propre approche à la machine à états finis en C. J'ai créé un script Python pour produire le code squelette C pour un certain nombre d'états. Ce script est documenté sur GituHub à FsmTemplateC

Cet exemple est basé sur d'autres approches que j'ai lues. Il n'utilise pas d'instructions goto ou switch mais a plutôt des fonctions de transition dans une matrice de pointeurs (table de correspondance). Le code repose sur une grande macro d'initialisation multi-lignes et des fonctionnalités C99 (initialiseurs désignés et littéraux composés), donc si vous n'aimez pas ces choses, vous n'aimerez peut-être pas cette approche.

Voici un script Python d'un exemple de tourniquet qui génère un code C squelette à l'aide de FsmTemplateC :

# 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)

L'en-tête de sortie généré contient les typedefs:

/* 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 *);
  • enum eFsmTurnstileCheckest utilisé pour déterminer si une transition a été bloquée avec EFSM_TURNSTILE_TR_RETREAT, autorisée à progresser avec EFSM_TURNSTILE_TR_ADVANCEou si l'appel de fonction n'a pas été précédé d'une transition avecEFSM_TURNSTILE_TR_CONTINUE .
  • énumérer eFsmTurnstileState est simplement la liste des états.
  • énumérer eFsmTurnstileInput est simplement la liste des entrées.
  • le FsmTurnstile structure est le cœur de la machine d'état avec la vérification de transition, la table de recherche de fonctions, l'état actuel, l'état commandé et un alias pour la fonction principale qui exécute la machine.
  • Chaque pointeur de fonction (alias) dans FsmTurnstilene doit être appelé qu'à partir de la structure et doit avoir sa première entrée en tant que pointeur sur lui-même afin de maintenir un état persistant, un style orienté objet.

Maintenant, pour les déclarations de fonction dans l'en-tête:

/* 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);

Les noms de fonction sont au format {prefix}_{from}_{to}, où {from}est l'état précédent (actuel) et {to}l'état suivant. Notez que si la table de transition ne permet pas certaines transitions, un pointeur NULL au lieu d'un pointeur de fonction sera défini. Enfin, la magie opère avec une macro. Ici, nous construisons la table de transition (matrice des énumérations d'état) et la table de recherche des fonctions de transition d'état (une matrice de pointeurs de fonction):

/* 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 \
}

Lors de la création du FSM, la macro FSM_EXAMPLE_CREATE() doit être utilisée.

Maintenant, dans le code source, chaque fonction de transition d'état déclarée ci-dessus doit être remplie. La FsmTurnstileFoptsstructure peut être utilisée pour transmettre des données vers / depuis la machine d'état. Chaque transition doit être définie fsm->checksur égale à soit EFSM_EXAMPLE_TR_RETREATpour l'empêcher de faire la transition, soit EFSM_EXAMPLE_TR_ADVANCEpour lui permettre de passer à l'état commandé. Un exemple de travail peut être trouvé sur (FsmTemplateC) [ https://github.com/ChisholmKyle/FsmTemplateC] .

Voici l'utilisation réelle très simple dans votre code:

/* 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);
}

Toutes ces affaires d'en-tête et toutes ces fonctions pour avoir une interface simple et rapide en valent la peine dans mon esprit.


0

Vous pouvez utiliser la bibliothèque open source OpenFST .

OpenFst est une bibliothèque pour la construction, la combinaison, l'optimisation et la recherche de transducteurs à états finis pondérés (FST). Les transducteurs à état fini pondérés sont des automates où chaque transition a une étiquette d'entrée, une étiquette de sortie et un poids. L'accepteur à états finis plus familier est représenté comme un transducteur avec chaque étiquette d'entrée et de sortie de transition égale. Les accepteurs à états finis sont utilisés pour représenter des ensembles de chaînes (en particulier, des ensembles réguliers ou rationnels); Les transducteurs à états finis sont utilisés pour représenter les relations binaires entre des paires de chaînes (en particulier, les transductions rationnelles). Les pondérations peuvent être utilisées pour représenter le coût d'une transition particulière.


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

Vous pouvez l'optimiser davantage pour la sécurité en utilisant un tableau de pointeurs à fonction constante vers les fonctions
AlphaGoku

0

J'utilise personnellement des structures d'auto-référencement en combinaison avec des tableaux de pointeurs. J'ai téléchargé un tutoriel sur github il y a quelque temps, lien:

https://github.com/mmelchger/polling_state_machine_c

Remarque: Je me rends compte que ce fil est assez ancien, mais j'espère obtenir des commentaires et des réflexions sur la conception de la machine à états ainsi que pouvoir fournir un exemple pour une conception possible d'une machine à états en C.


0

Vous pouvez considérer UML-state-machine-in-c , un framework de machine à états "léger" en C. J'ai écrit ce framework pour supporter à la fois la machine à états finis et la machine à états hiérarchique . Comparé aux tables d'états ou aux cas de commutation simples, une approche de cadre est plus évolutive. Il peut être utilisé pour des machines à états finis simples à des machines à états hiérarchiques complexes.

La machine d'état est représentée par la state_machine_tstructure. Il ne contient que deux membres "Event" et un pointeur sur "state_t".

struct state_machine_t
{
   uint32_t Event;          //!< Pending Event for state machine
   const state_t* State;    //!< State of state machine.
};

state_machine_tdoit être le premier membre de la structure de votre machine d'état. par exemple

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 contient un gestionnaire pour l'état et également des gestionnaires facultatifs pour l'action d'entrée et de sortie.

//! 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.
};

Si le cadre est configuré pour une machine à états hiérarchique, alors le state_t contient un pointeur vers l'état parent et enfant.

Framework fournit une API dispatch_eventpour envoyer l'événement à la machine d'état et switch_statedéclencher la transition d'état.

Pour plus de détails sur la façon d'implémenter une machine d'état hiérarchique, reportez-vous au référentiel GitHub .

exemples de code,

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


-1

Voici une méthode pour une machine d'état qui utilise des macros telles que chaque fonction peut avoir son propre ensemble d'états: https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code -à

Il s'intitule «simuler plusieurs tâches» mais ce n'est pas la seule utilisation.

Cette méthode utilise des rappels pour récupérer dans chaque fonction là où elle s'est arrêtée. Chaque fonction contient une liste d'états propres à chaque fonction. Une "boucle inactive" centrale est utilisée pour exécuter les machines d'état. La "boucle inactive" n'a aucune idée du fonctionnement des machines d'état, ce sont les fonctions individuelles qui "savent quoi faire". Afin d'écrire du code pour les fonctions, on crée simplement une liste d'états et utilise les macros pour "suspendre" et "reprendre". J'ai utilisé ces macros chez Cisco lorsque j'ai écrit la bibliothèque d'émetteurs-récepteurs pour le commutateur Nexus 7000.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.