Journal Et Dieu inventa le soutien gorge !

Posté par (page perso) . Licence CC by-sa
56
17
août
2012

Le C est connu pour ses pointeurs. Les pointeurs sont une merveille pour certains, une horreur pour d'autre. Je sais qu'il s'agit d'un nième débat religieux par ici, mais parlons de C et de pointeurs !

Un vrai moment de détente pour le week-end :D

Le noyau Linux utilise une forme particulière de listes chaînées qui nous permet d'apprécier ce genre de code include/linux/kernel.h:683:

#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})

Et quand mon apprenti tombe sur ce genre de truc, je me dois de lui expliquer.
Pour ceux qui se posent la question, ce petit code permet de trouver l'adresse d'une structure à l'aide de l'adresse d'un membre de la structure.

Imaginons la structure suivante :

struct x {
    int a;
    int b;
};

struct x X;

Et admettons :

  • X est à l'adresse 0x14C4
  • X.a est à l'adresse 0x14C8
  • X.b est à l'adresse 0x14CC

Donc, si ma structure X était à l'adresse 0x0000, nous aurions :

  • X.a à l'adresse 0x0004
  • X.b à l'adresse 0x0008

Donc si j'ai l'adresse de X.a, j'ai l'adresse de X, car :

&X == &(X.a) - ((struct x *)0)->a;

Tout ceci est bien connu et est disponible dans stddef.h à l'aide de la macro offsetof :

#include <stddef.h>

offsetof(struct x, a);

Tous ces éléments mis ensemble nous permettent d'obtenir des structures de données un peu différentes :

struct s_list {
    struct s_list * next;
    struct s_list * previous;
};

struct s_structure_x {
    /* membres */
    struct s_list list;
};

En lieu et place de :

struct s_structure_x {
    /* membres */
    struct s_structure_x * next;
    struct s_structure_x * previous;
};

ou encore :

struct s_list {
    void * data;
    size_t length;
    int type;
    struct s_list * next;
    struct s_list * previous;
};

et variantes.

Voilà, je suis pas certain d'être clair, mais je fais vite car ce n'est pas le sujet. En expliquant ce sujet, une idée m'est venue (et ça c'est tout l'intérêt de former des jeunes) pour mon projet actuel. Dans les grandes lignes, le projet est de faire communiquer des appareils entre eux.
Afin de faire ça proprement, j'ai, bien entendu, défini quelques couches, 3 en l'occurrence et chacune encapsule l'autre. Un couche réseau, une couche application et une couche donnée.

Dans la réflexion, le fait de travailler sur des micro-contrôleurs n'ayant que 512 octets de mémoires RAM et que la quantité de données transmises peut atteindre 150 octets, plus quelques 10 octets pour les couches, un encodage des données qui ajoutent une 20aine d'octets (1 bit perdu par octet transmis) et quelques variables d'état, l'utilisation de mémoire tampon pour le réseau est à proscrire ; on arrive presque à la moitié de la RAM juste pour les communications (dont la majeure partie n'est que ce qui se trouve déjà en mémoire mais sous une autre forme).

Le code réseau est donc prévu pour travailler en flux. Les données sont transformées et transmises à mesure. Quelques variables d'états plus tard, le réseau ne coûte qu'une ou deux dizaines d'octets en mémoire.

Le problème vient de l'enchaînement des couches dans le code, par exemple :

void hw_send( /* ... */ ) {
    /* ... */
    l1_send();
    /* ... */
}
void l1_send( /* ... */ ) {
    /* ... */
    l2_send();
    /* ... */
}
/* ... */

Ça marche, mais si je veux, par exemple, ajouter un traitement (chiffrement ?) entre la couche 1 et 2, le code doit être modifié de manière dramatique. Ou si je veux réutiliser la couche 2 dans une couche 1bis. Non ce qu'il me faut, c'est une sorte de liste chaînée qui traversent les couches et qui me permettent, le cas échéant, d'intercaler un traitement particulier.

Et du noyau vint la solution qui me semble, pour l'instant, la plus intéressante (on verra si ça tient jusqu'à lundi (dans mon esprit (dégénéré))). J'ai fais un petit test et ça donne ça :

#include <stdio.h>
#include <string.h>
#include <stddef.h>

/* Structure pour mes fonctions de communications 
 *
 * send et receive reçoivent en premier paramètre une variable 
 * (ComFunctions *). Le deuxième paramètre de send est une variable d'état,
 * quand elle est à 0xFF, il n'y a plus rien à envoyer. Pour receive, il 
 * s'agit de l'octet reçu par le réseau.
 * send retourne l'octet à transmettre et receive l'état qui, une fois à
 * 0xFF indique qu'il n'y a plus rien à recevoir.
 * La variable lower contient l'adresse de la couche en-dessous de l'actuelle.
 */
typedef struct s_com_funcs ComFunctions;
struct s_com_funcs {
    ComFunctions * lower;
    unsigned char (*send)(void *, unsigned char *);
    unsigned char (*receive)(void *, unsigned char);
};

/* Mes structures avec les valeurs nécessaires pour chaque couche ainsi
 * qu'une instance de ComFunctions.
 */
struct l1 {
    unsigned char val1;
    unsigned char val2;
    unsigned char val3;
    unsigned char state;
    ComFunctions f;
};

struct l2 {
    unsigned char val1;
    unsigned char val2;
    unsigned char state;
    ComFunctions f;
};

/* La fonction send. En paramètre, nous avons la couche la plus haute,
 * retourne 0 quand la transmission est terminée (pour permettre de faire
 * while(send(...));)
 */
char send(ComFunctions * highest)
{
    unsigned char res=0x00;
    printf("0x%02X ", highest->send(highest, &res));
    if(res==0xFF) {
        printf("\n");
        return 0;
    }
    return 1;
}

/* Des init bidons, juste pour le test */
void init_l2(struct l2 * me) { me->val1='Z'; me->val2='F'; me->state=0x00; }
void init_l1(struct l1 * me) { me->val1='a'; me->val2='b'; me->val3='c';
    me->state=0x00; }

/* Des fonctions send, juste un aperçu, mais le code est trivial */
unsigned char send_l2(void * com_funcs, unsigned char * state)
{
    struct l2 * me=(struct l2 *)((com_funcs)-offsetof(struct l2, f));
    /* ... */
}
unsigned char send_l1(void * com_funcs, unsigned char * state)
{
    struct l1 * me=(struct l1 *)((com_funcs)-offsetof(struct l1, f));
    unsigned char lower_state=0x00, tmp=0x00;

    switch(me->state) {
        /* ... plein de code ... */
        case 2:
            tmp='!';
            if(me->f.lower==NULL) {
                me->state++;
            } else {
                tmp=me->f.lower->send(me->f.lower, &lower_state);
                if(lower_state==0xFF) me->state++;
            }
            *state=me->state;
            return tmp;
        /* ... encore plein de code ... */
}

/* Une fonction main */
int main(int argc, char ** argv)
{
    /* Que deux couches, trop flemmard pour la troisième ^^ */
    struct l1 x;
    struct l2 y;

    init_l1(&x);
    init_l2(&y);

    x.f.lower=&(y.f);
    x.f.send=send_l1;
    y.f.lower=NULL;
    y.f.send=send_l2;

    while(send(&(x.f)));

    return 0;
}

Donc le fonctionnement est très simple, chaque couche peut appeler la couche sous-jacente et traiter son résultat avant de l'intégrer dans sa propre sortie. Ça permet d'intégrer des filtres entre les couches (j'ai testé avec un chiffrement fort (xor :-p), c'est démentiel).
Rien de bien révolutionnaire (déjà je travail pas chez Apple, c'est donc mal parti) et certainement déjà utilisé dans bien des projets, mais je cherchais juste une excuse pour faire un journal sur les pointeurs C (et les pointeurs de fonctions). Et ça fait jamais de mal de revoir un peu de C :)

Ah ! et je voulais aussi que ça ce sache : j'aime quand ça pointe !

  • # Moi aussi!

    Posté par (page perso) . Évalué à  1 .

    Ah ! et je voulais aussi que ça ce sache : j'aime quand ça pointe !

    Je préfère le Python au C, mais moi aussi, j'aime quand ça pointe!

    • [^] # Re: Moi aussi!

      Posté par . Évalué à  7 .

    • [^] # Re: Moi aussi!

      Posté par (page perso) . Évalué à  10 .

      Quand tu auras fais tourner Python avec 512 octets de RAM, 1Ko d'EEPROM et 8Ko de flash pour le code, je ferais mon code en Python.

      "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

      • [^] # Re: Moi aussi!

        Posté par (page perso) . Évalué à  0 .

        Quand tu auras fais tourner Python avec 512 octets de RAM, 1Ko d'EEPROM et 8Ko de flash pour le code, je ferais mon code en Python.

        Pas faux, mais tout dépend les besoins après. C'est sûr que je doute que python soit bon en tant que langage de haut niveau, au vu de la tache à accomplir avec si peu de ressources. C'est pour faire quoi en fait?

        • [^] # Re: Moi aussi!

          Posté par . Évalué à  2 .

          C'est pour faire quoi en fait?

          Il programme un micro-contrôleur, faut lire TOUS les commentaires ;)

          C'est sûr que je doute que python soit bon en tant que langage de haut niveau

          Tu voulais dire bas niveau plutôt je suppose.

          • [^] # Re: Moi aussi!

            Posté par . Évalué à  1 . Dernière modification : le 19/08/12 à 17:36

            Tu voulais dire bas niveau plutôt je suppose.

            Non, non, il voulait bien dire ce qu'il a dit. --> sort

        • [^] # Re: Moi aussi!

          Posté par (page perso) . Évalué à  6 .

          Faire des mesures, traiter ces mesures et les communiquer. Les valeurs sont sur 16 bits, il y'a une dizaine de capteurs ayant un identifiant sur 64 bits. On ajoute les valeurs max et min, donc 14 octets par capteurs, multiplié par 10, 140 octets de données pouvant être transférés et stockés en RAM et/ou EEPROM.
          Et c'est toujours un plaisir de coder sur des micro-contrôleurs, la différence entre un short et int prend tout son sens et il faut réfléchir dix fois avant de déclarer une variable : "En ai-je vraiment besoin ?"

          "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

          • [^] # Re: Moi aussi!

            Posté par . Évalué à  1 .

            "réfléchir dix fois avant de déclarer une variable "

            Euh ?! Ton compilateur ne fait pas le boulot ?

            Pour compacter à mort du code, le plus efficace que j'ai trouvé est l'inlining et le mot clef "static" (suivi des simplifications du compilo) pour éviter les .o où la moitié du code n'est pas utilisé et une fonctionnalité de récupération de fin de fonction par le compilateur. Je ne me rappelle plus le nom, mais en gros, si 2 fonctions se finissent de façon identique, l'une d'elle contient un goto vers la fin de la suivante. Cela permet de faire des choses, que la factorisation de code ne permet pas.

            "La liberté de tout dire n'a d'ennemis que ceux qui veulent se réserver le droit de tout faire". "La question n'est pas de savoir si vous avez quelque chose à cacher. La question est de savoir si c'est nous qui contrôlons le gouvernement ou l'inverse

            • [^] # Re: Moi aussi!

              Posté par (page perso) . Évalué à  5 .

              Euh ?! Ton compilateur ne fait pas le boulot ?

              Il ne va pas savoir si tu aurais pu utiliser un char ou short au lieu d'un int. Ensuite il y'a plein de trucs qui peuvent être utilisés :

              i=0;
              do {
                 i++;
              while(i<max);
              
              

              prend plus de place que :

              i=max;
              do {
                  i--;
              while(i);
              
              

              Ou encore préférer l'utilisation de switch-case au lieu de if-elseif-else.

              Ton compilateur fait beaucoup de travail, mais bien réfléchir l'aide.

              "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

              • [^] # Re: Moi aussi!

                Posté par . Évalué à  2 .

                Si tu parles de variable automatique, en déclarant un char, un short ou un int, le compilateur te collera un int (16 ou 32 bit selon la plateforme), en tout cas sur tous les compilos que j'ai vu.

                Concernant le retournement de compteur de boucle, il me semble que gcc fait ce genre de transformation dans toutes les boucles car la comparaison à zéro est toujours plus facile.

                Concernant le switch-case, j'aime beaucoup l'utiliser mais il utilise un tas de pointeur de fonction, ce qui peut être très lent sur les cpu "simples".

                Enfin, le plus simple est toujours de regarder la sortie assembleur pour voir ce que sort le compilateur.

                "La liberté de tout dire n'a d'ennemis que ceux qui veulent se réserver le droit de tout faire". "La question n'est pas de savoir si vous avez quelque chose à cacher. La question est de savoir si c'est nous qui contrôlons le gouvernement ou l'inverse

                • [^] # Re: Moi aussi!

                  Posté par (page perso) . Évalué à  2 .

                  Si tu parles de variable automatique, en déclarant un char, un short ou un int, le compilateur te collera un int (16 ou 32 bit selon la plateforme), en tout cas sur tous les compilos que j'ai vu.

                  Ça dépend. Si ton architecture est 8 bits, ton char va rester un char. Un short a des risques d'être transformé en int car pas de différence.
                  Dans tous les cas, chaque fois que je me documente sur le choix des types, le conseil est "utilisez le plus petit type possible".

                  Concernant le retournement de compteur de boucle, il me semble que gcc fait ce genre de transformation dans toutes les boucles car la comparaison à zéro est toujours plus facile.

                  Pas GCC 3.3.5. J'avais fait un travail d'optimisation sur un programme, l'inversion manuel des boucles apportaient un joli gain (mais ça date). Dans tous les cas, ça ne coûte rien de le faire soit même.

                  Concernant le switch-case, j'aime beaucoup l'utiliser mais il utilise un tas de pointeur de fonction, ce qui peut être très lent sur les cpu "simples".

                  La documentation du micro-contrôleur apporte ce genre d'info.

                  "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

                  • [^] # Re: Moi aussi!

                    Posté par . Évalué à  2 .

                    Dans tous les cas, chaque fois que je me documente sur le choix des types, le conseil est "utilisez le plus petit type possible".

                    Le compilo va prendre le plus rapide qui est en général la taille des registres.

                    Pas GCC 3.3.5. J'avais fait un travail d'optimisation sur un programme, l'inversion manuel des boucles apportaient un joli gain (mais ça date). Dans tous les cas, ça ne coûte rien de le faire soit même.

                    si des bugs, car c'est moins lisible. Parfois, le compilateur ne peut pas faire la transformation à cause de condition extérieur au fichier C ou à cause d'une variable d'induction.

                    La documentation du micro-contrôleur apporte ce genre d'info.

                    j'imagine. Mais dans le cas que j'avais en tête cela n'y était pas.

                    "La liberté de tout dire n'a d'ennemis que ceux qui veulent se réserver le droit de tout faire". "La question n'est pas de savoir si vous avez quelque chose à cacher. La question est de savoir si c'est nous qui contrôlons le gouvernement ou l'inverse

                • [^] # Re: Moi aussi!

                  Posté par . Évalué à  2 .

                  En fait, ça dépend.
                  Si tu fais un switch-case avec 0, 1, 2, 3… il va faire un tableau des adresses où sont les instructions suivante. Si tu as des valeurs trop différentes ou des grandes valeurs, il génère (le compilo) une dichotomie. Pour optimiser vraiment, il faut connaître la répartition des valeurs quand on rentre dans le switch.
                  Ainsi, si 8 est la valeur dans 90% des cas, il vaut mieux :

                  if (var==8)
                  {
                    /* Code */
                  }
                  else
                  {
                    switch(var)
                    {
                      case 2:
                    }
                  }
                  
                  

                  Sauf que là, l’auteur cherche à réduire la taille du code, enfin surtout de la RAM utilisé. Ce qui veut dire réduire la pile ce qui implique d’éviter trop de profondeur d’appel et de variables locales. L’optimisation en vitesse est généralement incompatible avec l’optimisation en taille.

                  • [^] # Re: Moi aussi!

                    Posté par . Évalué à  2 .

                    "L’optimisation en vitesse est généralement incompatible avec l’optimisation en taille."

                    Souvent mais pas toujours. L'inline est un bon exemple. En général, il est vu comme une augmentation de la taille du code. Dans un driver qui manipulait beaucoup de constantes, j'ai pu gagner beaucoup de place de code avec de l'inlining qui se simplifiait beaucoup (élimination de constante, etc…).

                    "La liberté de tout dire n'a d'ennemis que ceux qui veulent se réserver le droit de tout faire". "La question n'est pas de savoir si vous avez quelque chose à cacher. La question est de savoir si c'est nous qui contrôlons le gouvernement ou l'inverse

        • [^] # Re: Moi aussi!

          Posté par (page perso) . Évalué à  2 .

          512 octets de RAM,

          et moi qui me plaignait que 1 Ko c'était pas assez… J'ai un bolide avec mon extension 16K !

          ZX 16K RAM

          GNU's Not Unix / LINUX Is Not Unix Xernel

      • [^] # Re: Moi aussi!

        Posté par (page perso) . Évalué à  2 .

        Quand tu auras fais tourner Python avec 512 octets de RAM, 1Ko d'EEPROM et 8Ko de flash pour le code, je ferais mon code en Python.

        avez vous testé ce genre de chose :

        https://code.google.com/p/python-on-a-chip/

        • [^] # Re: Moi aussi!

          Posté par (page perso) . Évalué à  2 .

          Trop gros :

          • Requires roughly 55 KB program memory
          • Initializes in 4KB RAM; print "hello world" needs 5KB; 8KB is the minimum recommended RAM.

          "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

  • # 0xB16B00B5

    Posté par (page perso) . Évalué à  10 .

    En tout cas, si tu as besoin d'une constante arbitraire quelque part, Microsoft te montre la voie : 0xB16B00B5.

    • [^] # Re: 0xB16B00B5

      Posté par (page perso) . Évalué à  2 .

      D'ailleurs ça manque d'une nimage pour illustrer tout ça, ton article…

      • [^] # Re: 0xB16B00B5

        Posté par (page perso) . Évalué à  4 .

        Ça poserait des problèmes avec la modérations …

        "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

        • [^] # Re: 0xB16B00B5

          Posté par . Évalué à  7 .

          non, sans doute pas avec la modération, plutôt avec les pisses-froids qui n'apprécient pas l'humour potache de linuxfr et qui combattent le sexisme sous toutes ses formes (attention, pas trop de formes quand même, ça donne une image biasée de la femme)

          Only wimps use tape backup: real men just upload their important stuff on megaupload, and let the rest of the world ~~mirror~~ link to it

        • [^] # Re: 0xB16B00B5

          Posté par . Évalué à  4 .

          Je prends le risque : 0xb16b00b5

  • # ça marche !

    Posté par . Évalué à  10 .

    Ça me parait un peu compliqué, mais ça marche !
    Moi, je me serait contenté d'une liste chainée simple de pointeur de fonctions dans le genre :

    struct layer {
        void (*next_func)(struct layer*, ...);
        struct layer* next_layer;
    };
    
    

    et derrière, tes fonctions deviennent :

    void l1(struct layer* lay, ...) {
        ...
        lay->next_func(lay->next_layer, ...);
    }
    
    

    Tu peux facilement ajouter un truc au milieu de ta couche en ajoutant une fonction au milieu de liste layer.

    Et sinon, juste pour le fun :

    struct x {
        int a;
        int b;
    };
    
    struct x X;
    
    

    Et admettons :

    X est à l'adresse 0x14C4
    X.a est à l'adresse 0x14C8
    X.b est à l'adresse 0x14CC
    
    

    chez moi, l'adresse de X.a est la même que X ;)

    • [^] # Re: ça marche !

      Posté par (page perso) . Évalué à  6 .

      chez moi, l'adresse de X.a est la même que X ;)

      Oui, mais mon compilateur fait du padding avant le premier membre pour arranger mon explication !

      "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

      • [^] # Re: ça marche !

        Posté par . Évalué à  2 .

        Oui, mais mon compilateur fait du padding avant le premier membre pour arranger mon explication !

        Si tu as trouvé un compilateur qui accepte de faire du copinage, c'est très bien. :-)
        Mais autrement, d'une manière générale, la norme empêche les compilateurs de faire cela. Elle te garantit que le premier membre est bien aligné en début de structure et/ou d'union. Ça permet entre autres de faire des unions de structures commençant toutes par un champ « type », par exemple, qui permet de savoir tout de suite ce qu'il y a à l'intérieur, comme avec les XEvents de X-Window.

        • [^] # Re: ça marche !

          Posté par (page perso) . Évalué à  3 .

          C'est une habitude que j'ai prise avec les apprentis : je prends toujours des valeurs, fonctionnement, …, absurdes afin qu'ils ne prennent pas l'habitude de penser en terme de valeurs. Mes "bytes" font souvent 13 bits, par exemple, car rien ne m'empêche d'avoir un système qui fonctionne ainsi. Et pour mon ordinateur, le seul compilateur existant, c'est un compilateur qui met du padding avant le premier membre.
          J'essaie surtout de leur apprendre à penser de manière abstraite.

          "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

          • [^] # Re: ça marche !

            Posté par . Évalué à  3 .

            C'est une très bonne chose en soi ! Mais il est important de le faire uniquement pour les cas non définis par la norme ou alors explicitement marqués comme indéfinis par elle. Et en l'occurrence, dans le dernier draft de C99 (n1256.pdf), section § 6.7.2.1, on lit :

            6.7.2.1 Structure and union specifiers

            13
            Within a structure object, the non-bit-field members and the units in which bit-fields
            reside have addresses that increase in the order in which they are declared. A pointer to a
            structure object, suitably converted, points to its initial member (or if that member is a
            bit-field, then to the unit in which it resides), and vice versa. There may be unnamed
            padding within a structure object, but not at its beginning.

            Donc, tu es sûr que l'adresse du premier élément d'une structure sera toujours l'adresse de la structure elle-même avec tout compilateur qui se respecte (et qui, surtout, respecte la norme), ce qui permet d'échafauder de façon tout-à-fait officielle et sans warning des montages comme celui de XEvent.

            Ça s'explique également par le fait que le padding est généralement utilisé à des fins d'alignement et que, si tu dois aligner un membre, alors tu auras besoin d'aligner de la même façon la structure entière d'une manière générale.

    • [^] # Re: ça marche !

      Posté par (page perso) . Évalué à  5 .

      Moi, je me serait contenté d'une liste chainée simple de pointeur de fonctions dans le genre :

      struct layer {
      void (*next_func)(struct layer*, ...);
      struct layer* next_layer;
      };

      Sauf que chaque couche à sa propre structure avec ses propres données. Donc tu vas avoir :

      struct layer {
          void (*next_func)(struct layer *);
          void * opaque; /* ta structure pour les données nécessaire à ta couche */
          struct layer * next;
      };
      
      

      Et ça devient moins clair. On un void dans la structure au lieu d'un joli type qui nous guide dans le code. Ensuite, avec un petit jeu de fonctions, la partie pour traverser les couches va devenir une petite bibliothèques (à grand coup de macros). En utilisant la méthode du noyau, l'utilisateur de la bibliothèque peut facilement placer où il veut, en mémoire, ses structures, la bibliothèque n'en n'est pas gênée. Et quand tu n'as pas de MMU, ça peut aider.

      "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

      • [^] # Re: ça marche !

        Posté par (page perso) . Évalué à  2 .

        La solution évite le void * effectivement, mais elle a un autre inconvénient : il n'est pas évident à la lecture d'une partie du code (l'appel à la fonction send par exemple) que la fonction va utiliser les structures parentes du paramètre d'entrée de la fonction. Cela n'aide donc pas à la compréhension du code (je ne dis pas que le void * est particulièrement mieux non plus sur ce point…).

  • # Faire ça au runtime ? je préfère faire ça à la compilation.

    Posté par . Évalué à  10 .

    C'est bête de faire ça au runtime, alors que tu pourrai donner à ton compilateur toutes les informations nécessaire sur ta pile de couche pour qu'il te merge tout ça et te l'optimise au petit oignons, plutôt que de devoir parcourir une liste chaînée qui contient toujours les même valeurs.

    Parce que, ce que tu nous décris ressemble fortement à ce que l'on fait en C++ avec du Policy-based_design :

    #include <cstdio>
    
    template <class LowerLayer>
    struct l1 : LowerLayer {
            unsigned char val1 = 'a';
            unsigned char val2 = 'b';
            unsigned char val3 = 'c';
            unsigned char state = 0x00;
            unsigned char send(unsigned char* lower_state_jcomprend_pas_trop) {
                    unsigned char tmp = 0x00;
                    unsigned char lower_state;
                    // ... plein de code ...
                    switch (state) {
                    case 2:
                            // ici je considère que LowerLayer existe forcement,
                            // si ce n'est pas le cas, on pourrai passer une
                            // classe vide du genre NoLowerLayer avec des methodes
                            // vides qui seront éliminée par le compilo, on
                            // pourrai aussi tester avec
                            // if (std::is_same<LowerLayer,NoLowerLayer>::value)
                            // pour savoir s'il y a un niveau en dessous, ou faire
                            // du super SFINAE de ouf. mais bon, KISS.
    
                            tmp = this->LowerLayer::send(&lower_state);
                            if (lower_state == 0xFF)
                                    state++;
                            *lower_state_jcomprend_pas_trop = state;
                            return tmp;
                    }
                    // ... encore plein de code ...
    
            }
    };
    
    // J'imagine que l2 ne peut être qu'utilisée en couche la plus basse.
    // Sinon il faudra juste une autre classe 'terminale' du genre NoLowerLayer.
    struct l2 {
            unsigned char val1 = 'Z';
            unsigned char val2 = 'F';
            unsigned char state = 0x00;
            unsigned char send(unsigned char* lower) {
                    //...
            }
    
    };
    
    // Une couche intermédiaire, parce que je suis pas trop flemmard.
    template <class LowerLayer>
    struct l1_5 : LowerLayer {
            unsigned char send(unsigned char* lower) {
                    unsigned char lower_state;
                    *lower = 0x00;
                    puts("coucou maman");
                    return this->LowerLayer::send(&lower_state);
            }
    };
    
    template <class LayerStack>
    char send(LayerStack& stack) {
            unsigned char res = 0x00;
            stack.send(&res);
            if (res == 0xFF)
                    return 0;
            return 1;
    }
    
    int main() {
            // définition de mes couches : l1 puis l1,5 puis l2
            // et instanciation.
            l1<l1_5<l2>> ma_pile;
    
            while (send(ma_pile));
            return 0;
    }
    
    
    • [^] # Re: Faire ça au runtime ? je préfère faire ça à la compilation.

      Posté par (page perso) . Évalué à  3 .

      Je suis d'accord. Bien que je ne connaisse pas assez le C++ pour bien comprendre ton code, au final tout ce qui peut être pré-calculé à la compilation, le sera.
      Mais là le but, c'est plus l'approche. L'optimisation viendra ensuite. Mais d'abord un code qui marche bien pas optimisé, ensuite un code optimisé.

      Sinon pour
      > unsigned char send(unsigned char* lower_state_jcomprend_pas_trop) {

      les fonctions doivent retourner plusieurs valeurs : la valeur à transmettre et la progression de l'encodage (ou de décodage, je ne fais que la moitié du travail dans l'exemple) des trames :

      +----------+
      | L1 |
      | +------+ |
      | | L2 | |
      | | | |
      | +------+ |
      | |
      +----------+

      Je ne connais ni la taille de L1, ni celle de L2 avant la transmission, donc j'ai une valeur d'arrêt (0xFF). Toutes les autres valeurs peuvent être utilisé par les fonctions pour leur propre besoin. Dans un sens, je n'aurais pas besoin de remettre cette valeur dans la structure.

      "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

      • [^] # Re: Faire ça au runtime ? je préfère faire ça à la compilation.

        Posté par (page perso) . Évalué à  4 .

        Le C++ permet d'utiliser des templates, qui sont résolues à la compilation.

        Pour de l'embarqué à ce niveau il faut faire un tout petit peut attention à la taille que rajoute le C++, en général il suffit de désactiver le RTTI et tout roule !

        Les templates permettent de faire des horreurs, mais ça permet aussi de faire de très bonnes choses. Ça demande une conception assez différente d'une programmation classique et doit donc en général être imaginée dès le départ.

        • [^] # Re: Faire ça au runtime ? je préfère faire ça à la compilation.

          Posté par . Évalué à  2 .

          Plutôt que de comparer, çela a un usage de classe avec template, j'aurai tout simplement comparé son code au pattern adapter.

          C'est à dire chaque classe implémente une interface Layer.

          En plus de l'adapter, elle sont soit chainé entre elle, soit elle connaisse l'objet parent qui les agrège dans une liste de Layer.

          D'ailleurs dans ta structure, tu devrais songer au destructeur :).

          Enfin de souvenir, GTK est un regorge d'exemple de structure type objet composé de pointeur fonction qui sont chainé entre elle pour copier les mécanismes de l'objet (héritage, surcharge, etc …)

          • [^] # Re: Faire ça au runtime ? je préfère faire ça à la compilation.

            Posté par (page perso) . Évalué à  2 .

            D'ailleurs dans ta structure, tu devrais songer au destructeur :).

            Je suis sur un micro-contrôleur, pas d'allocation dynamique car pas de MMU. Pas besoin de destructeur :)

            "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

          • [^] # Re: Faire ça au runtime ? je préfère faire ça à la compilation.

            Posté par . Évalué à  6 .

            Je ne compare pas son code avec du chained-policy, je dit juste que, vu qu'il est dans l'embarqué, faire des choses au runtime est bête lorsqu'on peut faire la même chose à la compilation, surtout si ça élimine du code. Le policy-based design en C++ est juste un exemple de comment faire ça avec un langage moderne.

            L'équivalent en orienté objet ne serait pas vraiment Adapter (on adapte rien, c'est la même interface partout), mais plutôt une chaîne de pattern Strategy.

  • # Marre du hors-sujet.

    Posté par (page perso) . Évalué à  -3 .

    Ah ! et je voulais aussi que ça ce sache : j'aime quand ça pointe !

    Ce sexisme est intolérable. En outre, tu as oublié la nimage.

  • # Java

    Posté par (page perso) . Évalué à  0 .

    Je dirais :

    Et dieu créa la fête
    Sexe, accordéon, alcool

    Il faut bien ça pour comprendre ;)

    Born to Kill EndUser !

  • # Lapin compris

    Posté par (page perso) . Évalué à  2 .

    Bon, mon C est un peu rouillé, je suis passé au C++ depuis quelques années, et j'ai récemment pas fait tant de C++ que ça non plus, en fait.

    Mais je comprends pas du tout pourquoi il y a ce besoin d'accéder la structure parente d'une variable donnée, pourquoi ne pas directement donner la structure en paramètre?

    Ces lignes ne me paraissent pas logiques:

    x.f.lower=&(y.f);
    while(send(&(x.f)));
    
    

    J'aurai plutôt vu:

    x.f.lower=&y;
    while(send(&x));
    
    
    • [^] # Re: Lapin compris

      Posté par (page perso) . Évalué à  2 .

      x.f.lower=&y;
      

      Hmm, les types ne matchent pas dans ton assignation, t'as d'un côté un pointeur vers une structure ComFunctions et de l'autre un pointeur vers l2…
      Après ta remarque rentre peut être un peu dans le cas qu'il décrit là : https://linuxfr.org/nodes/95222/comments/1379891, où avec une structure layer comme celle là

      struct layer {
      void (*next_func)(struct layer *);
      void * opaque; /* ta structure pour les données nécessaire à ta couche */
      struct layer * next;};
      
      

      On pourrait écrire, si je dis pas de bêtises

      struct layer x,y;
      /*...*/
      x.next = &y
      
      
      • [^] # Re: Lapin compris

        Posté par (page perso) . Évalué à  5 .

        Oui voilà, j'ai oublié de supprimer le .f, disons que je comprends pas pourquoi le chainage se fait sur un membre de la structure et pas sur la structure, et surtout pourquoi on passe en paramètre un membre de la structure et pas la structure elle même.

  • # Économiser quelques octets

    Posté par . Évalué à  1 .

    me semble possible en changeant, dans struct l1 et l2, le membre f (de type ComFunctions) en un pointeur vers des ComFunctions, vu que si j'ai bien compris il y aurait une unique copie des ComFunctions pour chaque niveau.

    • [^] # Re: Économiser quelques octets

      Posté par (page perso) . Évalué à  3 .

      Non tu perds de la place :

      On va dire que toutes les variables font Z octets et que nous n'avons pas de bourrage, cas sans pointeur (6Z) :

      +-------[ largeur Z ]--------+
      |                            |
      V                            V
      +----------------------------+
      | l1.val1                    | -> adresse X
      | l1.val2                    |
      | l1.val3                    |
      | l1.ComFunctions.lower      |
      | l1.ComFunctions.send       |
      | l1.ComFunctions.receive    |
      +----------------------------+
      
      

      cas avec pointeur (7Z):

      +-------[ largeur Z ]--------+
      |                            |
      V                            V
      +----------------------------+
      | l1.val1                    | -> adresse X
      | l1.val2                    |
      | l1.val3                    |
      | l1.ComFunctions * Y        |
      +----------------------------+
      |                            |
      //                          //
      |                            |
      +----------------------------+
      | {l1}->ComFunctions.lower   | -> adresse Y
      | {l1}->ComFunctions.send    |
      | {l1}->ComFunctions.receive |
      +----------------------------+
      
      

      Pour chaque structure (l1, l2, …) tu dois avoir une structure ComFunctions differentes.

      "It was a bright cold day in April, and the clocks were striking thirteen" - Georges Orwell

      • [^] # Re: Économiser quelques octets

        Posté par . Évalué à  1 .

        Donc il y a une seule instance de structure pour chaque niveau… effectivement dans ce cas le pointeur est inutile.

  • # Y aurait pas une 'tite erreur?

    Posté par . Évalué à  2 .

    Ne serait-ce pas

        &X == &(X.a) - &(((struct x *)0)->a);
    
    

    au lieu de

        &X == &(X.a) - ((struct x *)0)->a;
    
    

    ?

Suivre le flux des commentaires

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