Coroutine en C

Bonjour,

Beaucoup de langages modernes ont une fonctionnalité que je trouve intéressante. Il s’agit des coroutines.

Pour simplifier, une coroutine est une boucle dans laquelle on peut sortir temporairement, sans en perturber la reprise.

Dans la plupart des langages, on remplace ‘return’ par ‘yield’.

Le langage C n’aime pas l’évolution. Je peux le comprendre, car il faut qu’il garde son statut du « plus bas niveau langage de haut niveau ».

L’avantage des coroutines, c’est qu’on peut séparer la génération de la donnée de son traitement.

De plus, on ne limite pas la génération à quelques valeurs, mais on peut faire une boucle infinie sans se soucier de combien il en faut.

C’est un bon moyen de rendre le code réutilisable.

Dans cet article, nous allons voir comment afficher des nombres séquentiellement. Sans coroutine, puis nous allons voir une méthode de transformation.

Voici un exemple d’un affichage des nombres premiers :

#include <malloc.h>
#include <stdio.h>
typedef struct _Doublet{
   int valeur;
   int compteur;
}Doublet;
int main(int argc, char *argv[]){
   int tabNb=0;
   Doublet *tab=0;
   for(int i=2;i<100;i++){
      int j;
      for(j=0;j<tabNb;j++){
         while(tab[j].compteur<i){
            tab[j].compteur+=tab[j].valeur;
         }
         if(tab[j].compteur==i){
            //pas premier
            break;
         }
      }
      if(j==tabNb){
         //premier
         tabNb++;
         tab=realloc(tab, sizeof(*tab)*tabNb);
         tab[tabNb-1].valeur=i;
         tab[tabNb-1].compteur=i;
         printf("%d\n",i);
      }
   }
   free(tab);
   tab=0;
}

J’ai fais un algorithme qui garde trace des nombres premiers trouvés pour tester les suivants. Rien de bien évolué, mais il y a une structure et un tableau. Ce qui montrera que la transformation ne marchera pas seulement pour les algorithmes triviaux, mais aussi dans les cas plus complexes.

L’étape suivante est de séparer la déclaration des variables, leur initialisation et la destruction dans des blocs distincts.

#include <malloc.h>
#include <stdio.h>
typedef struct _Doublet{
   int valeur;
   int compteur;
}Doublet;
int main(int argc, char *argv[]){
   //declaration
   int tabNb;
   Doublet *tab;
   int i;
   int j;
   //initialisation
   tabNb=0;
   tab=0;
   //traitement
   for(i=2;i<100;i++){
      for(j=0;j<tabNb;j++){
         while(tab[j].compteur<i){
            tab[j].compteur+=tab[j].valeur;
         }
         if(tab[j].compteur==i){
            //pas premier
            break;
         }
      }
      if(j==tabNb){
         //premier
         tabNb++;
         tab=realloc(tab, sizeof(*tab)*tabNb);
         tab[tabNb-1].valeur=i;
         tab[tabNb-1].compteur=i;
         printf("%d\n",i);
      }
   }
   //destruction
   free(tab);
   tab=0;
}

Rien de bien sorcier, il faut juste être rigoureux pour ne rien oublier.

Maintenant, il faut mettre toutes les déclarations dans une structure, et utiliser cette structure bien entendu. J’ai appelé cette structure ‘Contexte’

#include 
#include 
typedef struct _Doublet{
   int valeur;
   int compteur;
}Doublet;
typedef struct _Contexte{
   int tabNb;
   Doublet *tab;
   int i;
   int j;
}Contexte;
int main(int argc, char *argv[]){
   //init contexte
   Contexte *c=0;
   c=malloc(sizeof(*c));
   //initialisation
   c->tabNb=0;
   c->tab=0;
   //traitement
   for(c->i=2;c->i<100;c->i++){
      for(c->j=0;c->jtabNb;c->j++){
         while(c->tab[c->j].compteuri){
            c->tab[c->j].compteur+=c->tab[c->j].valeur;
         }
         if(c->tab[c->j].compteur==c->i){
            //pas premier
            break;
         }
      }
      if(c->j==c->tabNb){
         //premier
         c->tabNb++;
         c->tab=realloc(c->tab, sizeof(*c->tab)*c->tabNb);
         c->tab[c->tabNb-1].valeur=c->i;
         c->tab[c->tabNb-1].compteur=c->i;
         printf("%d\n",c->i);
      }
   }
   //destruction
   free(c->tab);
   c->tab=0;
   //fin contexte
   free(c);
   c=0;
}

On va créér des fonctions pour chacune des parties du code : initialisation, traitement, destruction. On pourra laisser le compilateur gérer l’init et la destruction de la structure ‘c’.

#include 
#include 
typedef struct _Doublet{
   int valeur;
   int compteur;
}Doublet;
typedef struct _Contexte{
   int tabNb;
   Doublet *tab;
   int i;
   int j;
}Contexte;
void init(Contexte *c){
   c->tabNb=0;
   c->tab=0;
}
void premier(Contexte *c){
   for(c->i=2;c->i<100;c->i++){
      for(c->j=0;c->jtabNb;c->j++){
         while(c->tab[c->j].compteuri){
            c->tab[c->j].compteur+=c->tab[c->j].valeur;
         }
         if(c->tab[c->j].compteur==c->i){
            //pas premier
            break;
         }
      }
      if(c->j==c->tabNb){
         //premier
         c->tabNb++;
         c->tab=realloc(c->tab, sizeof(*c->tab)*c->tabNb);
         c->tab[c->tabNb-1].valeur=c->i;
         c->tab[c->tabNb-1].compteur=c->i;
         printf("%d\n",c->i);
      }
   }
}
void clean(Contexte *c){
   free(c->tab);
   c->tab=0;
}
int main(int argc, char *argv[]){
   Contexte c;
   //initialisation
   init(&c);
   //traitement
   premier(&c);
   //destruction
   clean(&c);
}

Enfin, la dernière étape, ajouter un attribut yield, utiliser goto (oui, c’est mauvais, mais c’est ça ou le duff device, beuurk !) et utiliser notre coroutine.

#include 
#include 
typedef struct _Doublet{
   int valeur;
   int compteur;
}Doublet;
typedef struct _Contexte{
   int tabNb;
   Doublet *tab;
   int i;
   int j;
   int yield;
}Contexte;
void init(Contexte *c){
   c->tabNb=0;
   c->tab=0;
   c->yield=0;
}
int premier(Contexte *c){
   switch(c->yield){
      case 1: goto yield1;
   }
   for(c->i=2;;c->i++){
      for(c->j=0;c->jtabNb;c->j++){
         while(c->tab[c->j].compteuri){
            c->tab[c->j].compteur+=c->tab[c->j].valeur;
         }
         if(c->tab[c->j].compteur==c->i){
            //pas premier
            break;
         }
      }
      if(c->j==c->tabNb){
         //premier
         c->tabNb++;
         c->tab=realloc(c->tab, sizeof(*c->tab)*c->tabNb);
         c->tab[c->tabNb-1].valeur=c->i;
         c->tab[c->tabNb-1].compteur=c->i;
         c->yield=1;
         return c->i;
         yield1:;
      }
   }
}
void clean(Contexte *c){
   free(c->tab);
   c->tab=0;
}
int main(int argc, char *argv[]){
   Contexte c;
   //initialisation
   init(&c);
   //traitement
   for(int i=0;i<25;i++){
      printf("%d\n",premier(&c));
   }
   //destruction
   clean(&c);
}

Je ne veux pas intégrer ‘init’ et ‘clean’ dans le traitement à cause de ‘clean’ qui ne serait jamais appelé. Et une destruction explicite sans init me semble bizarre.

Voila, on a une coroutine pas trop dégueux à utiliser et qui reste fidèle à l’algorithme de départ.