Journal Flex mon ami !

Posté par  .
Étiquettes : aucune
0
31
jan.
2005
** toutes mes excuses pour le formattage de code foireux
** mais je n'obtiens pas les tabulations avec les balises code
** Donc j'utilise pre mais du coup les inf et sup des include sont pas
** affichés. J'espère que c'est un problème du à la css que j'utilise...
** Les lignes vides sont une des conséquences aussi...

De retour avec de nouvelles aventures à propos de flex, c'est outil
magnifique qui vous fait gagner du temps, de la sécurité, et de la
maintenabilité. Mais passons cette description qui bien sûr ne
satisfera pas tout le monde. Mais je m'en cogne. Je viens juste ici
vous livrez un peu de mon expérience à avec outil.


Un peu de 'background', j'ai toujours trouvé l'écriture de parser
très très lourd. Je me suis rendu compte très vite que pour faire
quelquechose de maintenable c'était impossible, car personne
n'avait le courage de replonger le nez dans ses lignes, même moi
l'auteur détestait me rendre compte que je devais y retourner.
Un beau jour, au gré de mes balades internet, il y a un moment je
tombe sur flex, et plus tard bison. Etant déjà grand utilisateur de
perl pour ses expressions rationnelles, j'ai trouvé l'approche très
intéressante de flex. Aimant la performance et ne supportant pas
savoir que ma chaine de caractère va être dupliquée X fois en
mémoire juste pour matcher un simple motif, j'ai vite abandonné
perl pour parser quoi que ce soit. L'énorme avantage de flex c'est
qu'il ne fonctionne pas par ligne. C'est beaucoup plus simple de
trouver des liens web dans une seule ligne qu'en Perl. Et c'est en
plus beaucoup plus efficace...


Une fois la maîtrise de l'outil, je me disais que c'était bien la
classe mais j'aurais vraiment voulu essayer d'étendre ses
possibilités autre part que sur des fichiers ... Sur des socket ca
serait fort intéressant. C'est à la suite du developpement d'un
serveur IMAP dont le parseur est écrit intégralement en flex/bison,
que me suis heurté au problème spécifique des sockets et de la
programmation réseau. C'est à dire, que tous les gens qui me
parlent ne pensent qu'a tenter de faire des overflow sur mes
buffer, et qu'ils prennent un malin plaisir à ouvrir des connexions
sans rien n'y faire.
C'est constation faite, je trouvais insoutenable de me voir ainsi à
la merci de n'importe qui. Il me fallait une solution. C'est une
mission pour moi !

Je vais donc vous présenter ici ma méthode pour rendre flex,
utilisable comme moteur principal d'une application utilisant un
protocole bien défini. Au hasard HTTP...

Cependant avant de passer au HTTP, je vais le faire sur un exemple
très simple. Un protocole utilisant des headers déjà très courant
sur internet ;p.

Récapitulons ce que nous cherchons à faire:
- lire un flux de données, avec un timeout
- lire un flux de données en controlant leur taille.
Ce qui veut dire que notre serveur ne dois attendre des données que
pendant un certains temps, et doit pouvoir vérifier qu'il n'a pas
reçu trop de données...
Afin de rendre possible ces deux choses nous avons besoin de rendre
notre fonction flex capable d'attendre des données seulement
pendant une durée définie, et capable de savoir combien de données
il me reste à lire. Pour la première chose c'est très simple, il
suffit de passer le fd en O_NONBLOCK et d'appeler poll dessus avant
de faire un read(). Pour la seconde chose nous allons utilisez ce
que le protocole ont prévus pour cela, c'est à dire le
Content-Length...

Pour bien comprendre ce que nous allons faire, il faut juste noter
que nous allons faire avec flex un parseur capable de s'arréter
s'il n'a rien lu, et surtout d'extraire du flux lui même la
quantité de données à lire.
Pour les paranoiaques comme moi, on notera qu'il est très important
de fixer une limite à la quantité des données à lire, il est hors
de question d'accepter toutes les valeurs...

Assez de blah blah, entrons dans le code. Première chose on
redéfini la fonction de lecture de flex !


#define YY_INPUT(buf, result, max_size) \
do { \
int ret = 0; \
struct pollfd fds; \
fds.fd = 0; \
fds.events = POLLIN; \
if ( ! poll(&fds, 1, 5000) ) { \
fatal = 1; \
result = 0; \
YY_FATAL_ERROR("input in flex scanner failed timeout reading") \
goto skip_reading; \
} \
if ( fds.revents & ( POLLOUT | POLLERR | POLLHUP ) ) { \
fatal = 1; \
result = 0; \
goto skip_reading; \
} \
if ( (result = read( 0, (char *) buf, max_size )) < 0 ) { \
fatal = 1; \
result = 0; \
YY_FATAL_ERROR( "input in flex scanner failed" ) \
} \
skip_reading: \
} while (0)


Ainsi flex va utiliser ce code pour lire des données, les attentifs
auront notés que ce code ne contient pas le passage du fd en
O_NONBLOCK, enfait c'est fait avant d'appeler yylex(). La
différence fondamentale entre le faire et ne pas le faire c'est que
le read sera bloquant ou pas, par contre il n'y aura aucune
différence sur la fonction poll(). Dans certains cas, il est
possible de ne pouvoir lire des données après la sortie d'une
fonction de monitoring de fd (poll(), select(), kqueue(), sigio,
epoll() etc). Donc dans un cas le read retourner -1 et errno a
EAGAIN, alors que dans l'autre il va bloquer jusqu'à obtenir des
données. Ce sont vos tests qui décideront de la validité de mettre
le fd en O_NONBLOCK, c'est pour cela que ce n'est pas inclu dans la
macro.
Autre problème, la fonction YY_FATAL_ERROR, sort définitivement du
lexer. Moi je ne veux pas qu'un timeout me fasse sortir, donc je
redifini la fonction YY_FATAL_ERROR.

#define YY_FATAL_ERROR(msg) fatal_error(msg);

J'aurais bien sur inseré cette ligne avant la macro YY_INPUT...
Le code de ma fonction fatal_error est le suivant

static void fatal_error(const char *msg)
{
printf("fatal error: %s\n", msg);
fatal = 1;
}

Vous aurez compris, je pense, que ma variable fatal est du type
extern volatile int, et est utilisée dans la boucle principale du
programme appelant la fonction yylex(). C'est un exemple, a vous
d'adapter en fonction de vos besoins/idées.

Maintenant passons à l'étape, lecture de données bornée. Flex nous
mets à disposition la fonction input() qui va appeler notre
fonction de lecture si besoin est, et nous retourner le caractère
suivant. C'est elle qui va être au coeur de notre gestion de
donnée... Et voici comment:

T_CONTENT_SIZE ^content-size:\ [0-9]+$
T_END_OF_HEADER \n\n

%%

{T_CONTENT_SIZE} {

bytes = atoi((const char *) &yytext[14]);
if ( bytes < 0 )
bytes = 0;

printf("+ CS %d\n", bytes);
}

{T_END_OF_HEADER} {
already_read = 0;
int ret;

buffer = malloc(sizeof(char) * bytes);
if ( buffer ) {
for ( already_read = 0; already_read < bytes; already_read++ ) {
ret = input();
if ( (ret == EOF) || (ret == 0) || fatal ) {
printf("error: end of input after reading only %d bytes.\n", already_read);
yyterminate();
break;
}
buffer[already_read] = ret;
}
}
}

Alors, nous utilisons content-size, pour le fun, changer le en
content-length ca marche autant ... Ensuite nous utilisons le motif
T_END_OF_HEADER afin de marquer le passage entre headers et data.
C'est très proche du HTTP... C'est voulu.
Dans le flux lorsque nous trouvons T_CONTENT_SIZE, nous récupérons
la valeur, (il faut checker errno avec atoi pour différencier de 0
et de 0 à cause d'une erreur, ne faites pas comme moi ...) nous la
stockons dans bytes.
Après à la fin de headers après T_END_OF_HEADER, nous allouons
assez de mémoire pour recevoir les données. Une simple boucle
permet de remplir le buffer tout justement alloué. Remarquer bien
que nous ne lisons pas plus que bytes... Nous n'écrasons rien.
(On peut/_doit_ tout a fait prévoir une vérification plus solide de bytes,
en la bornant à un certain maximum...)
Nous testons tous les valeur de retour de input() afin d'agir en
conséquence et de gérer une éventuelle déconnexion, ou erreur de
lecture. Libre à vous encore une fois d'appeler yyterminate() ou
pas.

Voici le code entier du programme compilable de cette manière:

- flex -Cr -i test.lex
- gcc lex.yy.c -o input_test


test.lex

{
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <poll.h>


static unsigned long bytes;
static unsigned long already_read;
static char *buffer;

static int fatal;
static void fatal_error(const char *msg);

#define YY_FATAL_ERROR(msg) fatal_error(msg);

#define YY_INPUT(buf, result, max_size) \
do { \
int ret = 0; \
struct pollfd fds; \
fds.fd = 0; \
fds.events = POLLIN; \
if ( ! poll(&fds, 1, 5000) ) { \
fatal = 1; \
result = 0; \
YY_FATAL_ERROR("input in flex scanner failed timeout reading") \
goto skip_reading; \
} \
if ( fds.revents & ( POLLOUT | POLLERR | POLLHUP ) ) { \
fatal = 1; \
result = 0; \
goto skip_reading; \
} \
if ( (result = read( 0, (char *) buf, max_size )) < 0 ) { \
fatal = 1; \
result = 0; \
YY_FATAL_ERROR( "input in flex scanner failed" ) \
} \
skip_reading: \
} while (0)

%}

T_CONTENT_TYPE ^content-type:\ [^ \n]+$
T_CONTENT_SIZE ^content-size:\ [0-9]+$
T_END_OF_HEADER \n\n

%%

{T_CONTENT_TYPE} {
printf("+ CT %.*s\n", yyleng - 14, &yytext[14]);
}

{T_CONTENT_SIZE} {

bytes = atoi((const char *) &yytext[14]);
if ( bytes < 0 )
bytes = 0;

printf("+ CS %d\n", bytes);
}

{T_END_OF_HEADER} {
already_read = 0;
int ret;

buffer = malloc(sizeof(char) * bytes);
if ( buffer ) {
for ( already_read = 0; already_read < bytes; already_read++ ) {
ret = input();
if ( (ret == EOF) || (ret == 0) || fatal ) {
printf("error: end of input after reading only %d bytes.\n", already_read);
yyterminate();
break;
}
buffer[already_read] = ret;
}
}
}

%%

static void fatal_error(const char *msg)
{
printf("fatal error: %s\n", msg);
fatal = 1;
}

int main()
{
fatal = 0;
buffer = 0;

yyin = stdin;
yylex();

printf("found %d bytes of data:\n%.*s\n", already_read, already_read, buffer);

if ( buffer )
free(buffer);
return 0;
}


En espérant que ça puisse vous vous inciter à coder
vos parsers avec des outils fait pour. Ca nous éviterait d'avoir
un nombre incalculable d'overflow dans les moindres petites fonctions
d'analyses de flux/caractères... Le libre en aurait bien besoin...
  • # Euuuuuh...

    Posté par  . Évalué à 1.

    Dis moi, t'as pas l'impression de te compliquer la vie pour rien avec l'utilisation de poll() etc. Sachant que tu peux envoyer à yylex() un FILE *, pourquoi ne pas en profiter ?

    Sinon, je confirme, flex est un vrai bonheur à utiliser pour ce qui est du parsage de données. Bon, évidemment, ça ne fait pas des miracles et il faut faire attention à ce qu'on écrit, mais on peut faire des choses bien belles :)
    • [^] # Re: Euuuuuh...

      Posté par  . Évalué à 1.

      > Dis moi, t'as pas l'impression de te compliquer la vie pour rien avec
      > l'utilisation de poll() etc. Sachant que tu peux envoyer à yylex() un FILE
      > *, pourquoi ne pas en profiter ?

      Je ne comprends pas ce que tu veux dire. Quel est le rapport entre timeout, et FILE ?

      Raconte moi tout...
      • [^] # Re: Euuuuuh...

        Posté par  . Évalué à 1.

        En fait, ce que je me demandais, c'est "mais pourquoi il met un timeout, alors qu'il pourrait très bien récupérer son bousin dans un fichier, ce qui évite tout plein de tours de passe passe qui sortent un peu du cadre de flex".

        Et puis, j'ai réflechi, et je me suis dit : ha mais si c'est pour un flux, effectivement, ma réflexion n'est pas intelligente. Simplement, hier j'ai oublié de faire part de cette dernière ellucubration de l'esprit, donc c'est fait aujourd'hui.
        (sisi, des fois ça me trotte dans la tête pendant quelques jours ces histoires)
  • # Un ROT13 en flex !!!

    Posté par  . Évalué à 3.

    Bien sûr, c'est pour le fun (et pas portable, cf. les "+13" et "-13"), mais bon, assez agréable et vite codé :
    %option noyywrap
    %option case-insensitive
    %option warn
    %option nounput

    %{
    /* #define YY_MAIN (0) */
    #include <stdio.h>
    #include <stdlib.h>

    %}

    %s COMMENT

    %%
    [A-Ma-m] { fprintf(stdout,"%c", (char)(yytext[0]+13) ) ; }
    [N-Zn-z] { fprintf(stdout,"%c", (char)(yytext[0]-13) ) ; }
    %%

    int main( int argc, char * argv[] )
    {
    if (argc > 1) {
    yyin = fopen ( argv[1], "r" ) ;
    } else {
    yyin = stdin ;
    }

    yylex () ;

    if (argc > 1) {
    fclose ( yyin ) ;
    }

    return( 0 ) ;
    }

    On compile avec : flex -orot13.c rot13.lex
    puis gcc sur rot13.c

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.