DEV Community

DURAND Malo
DURAND Malo

Posted on

Obtenir une structure depuis un membre en C

Bonjour. Aujourd'hui, on va parler pointeurs et structures en langage C. Nous allons tenter, depuis un pointeur vers le membre d'une structure, de récupérer cette structure.

Introduction

Cas de figure

Nous allons considérer cette structure :

typedef struct s_person {
    unsigned long long id;
    char const *name;
    unsigned int age;
    void (*get_info)(struct s_person *p);
} Person;

void get_info(Person *p)
{
    printf("Myself ID: %llu (0x%llx)\n", p->id, p->id);
    printf("Myself Name: %s\n", p->name);
    printf("Myself Age: %u\n", p->age);
    printf("Myself Address: %p\n", (void *) p);
}
Enter fullscreen mode Exit fullscreen mode

Ainsi que cette instance :

Person myself = {
    .id = (unsigned long long) 0xdeadbeef,
    .name = "Malo",
    .age = 19,
    .get_info = get_info
};
Enter fullscreen mode Exit fullscreen mode

L'objectif

Avec toutes ces informations, nous allons implémenter une fonction appelée get_person_handle_by_name dont l'objectif est, à partir d'un pointeur vers un member name d'une instance de type Person, de récupérer un pointeur vers cette instance.

Pour que ça soit plus claire, voici le prototype de la fonction :

Person *get_person_handle_by_name(char const **name);
Enter fullscreen mode Exit fullscreen mode

name étant ici une référence à Person.name par exemple.

Comment c'est possible ?

En utilisant un langage bas niveau, le travail sur la mémoire est rendu plus simple. Concrètement, lorsque vous définissez une structure, une taille lui est associée en fonction de ses membres.

Par exemple, la taille de notre structure s_person est égale à la taille des membres id, name, age et get_info, ce qui correspond à 8 + 8 + 4 + 8 = 28.

Le padding des structures

Lorsque vous déclarez une structure, le compilateur va calculer la taille qu'elle occupe en mémoire en fonction de ses membres, de la même manière que ce que nous venons de faire.

Cependant, il ne s'arrête pas là, puisque si la taille n'est pas divisible par 8 (système 64 bits) ou par 4 (système 32 bits), il calcule la différence d'espace pour l'ajouter à la taille totale. Cela permet d'aligner la mémoire et simplifie grandement la tâche au processeur qui souhaite accéder à la structure.

Pour observer ce comportement, vous pouvez tester ceci :

typedef struct s_person {
    unsigned long long id;
    char const *name;
    unsigned int age;
    void (*get_info)(struct s_person *p);
} Person;

/* Indique au compilateur de ne pas aligner la mémoire */
#pragma pack(1)
typedef struct s_person_packed {
    unsigned long long id;
    char const *name;
    unsigned int age;
    void (*get_info)(struct s_person_packed *p);
} PersonPacked;

printf("sizeof(Person): %zu\n", sizeof(Person));
printf("sizeof(PersonPacked): %zu\n", sizeof(PersonPacked));

/*
Sortie :
sizeof(Person): 32
sizeof(PersonPacked): 28
*/
Enter fullscreen mode Exit fullscreen mode

Cette information n'est pas capitale pour ce que nous allons faire, mais il est bon de savoir comment est stockée une structure en mémoire.

Accéder à un membre depuis la structure

Reprennons cette exemple :

Person myself = {
    .id = (unsigned long long) 0xdeadbeef,
    .name = "Malo",
    .age = 19,
    .get_info = get_info
};
Enter fullscreen mode Exit fullscreen mode

Si vous affichez l'adresse de myself, et l'adresse de myself.id, quelque chose devrait vous choquer :

printf("&myself : %p\n", (void *) &myself);
printf("&myself.id : %p\n", (void *) &myself.id);
/*
Sortie :
&myself : 0x16fb96f40
&myself.id : 0x16fb96f40
*/
Enter fullscreen mode Exit fullscreen mode

En effet, l'adresse de l'instance de la structure, et l'adresse du premier membre sont identiques. Il est donc techniquement possible de faire ça :

printf("Ox%llx\n", *((unsigned long long *) (&myself)));
/*
Sortie :
0xdeadbeef
*/
Enter fullscreen mode Exit fullscreen mode

Ou encore ça :

void (*_get_info)(Person) = (void *) * (void **) ((void *) &myself + 24ULL);
_get_info(myself);
/*
Sortie :
Myself ID: 3735928559 (0xdeadbeef)
Myself Name: Malo
Myself Age: 19
Myself Address: 0x16ae9af00
*/
Enter fullscreen mode Exit fullscreen mode

Eh oui, on peut aussi accéder aux fonctions. Notez bien les conversions en void * et void **, cela permet d'ajouter 24ULL à l'adresse de myself puisque la fonction membre get_info se situe à &myself + 24ULL.

Le soucis est que si on essaie d'ajouter 24ULL sans passer par ces conversions, c'est en réalité 24 * 8 qui sera ajouté.

Vous aurez une petite surprise si vous essayé d'exécuter une fonction depuis une mauvaise adresse.

Accéder à la structure depuis un membre

Maintenant que vous avez tous les concepts en tête, on peut commencer à attaquer le vif du sujet.

Souvenez vous de cette fonction :

Person *get_person_handle_by_name(char const **name);
Enter fullscreen mode Exit fullscreen mode

Son premier paramètre est un pointeur vers le member name d'une instance de type Person. Voici l'algorithme qui sera à mettre en place :

  • Déterminer la différence entre l'adresse du membre name et celle du début (ou du premier élément) d'une instance Person.
  • Utiliser cette différence pour la soustraire à l'adresse du paramètre name de la fonction, pour retrouver l'instance associée.
  • Retourner le pointeur de l'instance.
/* Uniquement pour avoir un code plus court. */
typedef unsigned long long ull;

Person *get_person_handle_by_name(char const **name)
{
    /* Création d'une instance quelconque */
    Person handle;

    /* On détermine la différence entre les deux adresses : */
    ull offset = (ull *) &handle.name - (ull *) &handle;

    /* On soustrait la différence pour obtenir le parent de `name`. */
    return (Person *) (name - offset);
}
Enter fullscreen mode Exit fullscreen mode

Et voilà ! C'est tout. Rien de bien méchant. Vous pouvez maintenant tester ce code :

#include <stdio.h>

typedef struct s_person {
    unsigned long long id;
    char const *name;
    unsigned int age;
    void (*get_info)(struct s_person *p);
} Person;

void get_info(Person *p)
{
    printf("Myself ID: %llu (0x%llx)\n", p->id, p->id);
    printf("Myself Name: %s\n", p->name);
    printf("Myself Age: %u\n", p->age);
    printf("Myself Address: %p\n", (void *) p);
}

typedef unsigned long long ull;

Person *get_person_handle_by_name(char const **name)
{
    Person handle;
    ull offset = (ull *) &handle.name - (ull *) &handle;

    return (Person *) (name - offset);
}

int main(void)
{
    Person myself = {
        .id = (unsigned long long) 0xdeadbeef,
        .name = "Malo",
        .age = 19,
        .get_info = get_info
    };
    Person *handle = get_person_handle_by_name(&myself.name);

    printf("------ Direct Reference ------\n");
    myself.get_info(&myself);
    printf("------------------------------\n");
    printf("\n");
    printf("------ Handle Reference ------\n");
    handle->get_info(handle);
    printf("------------------------------\n");
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Voici une sortie que vous devriez obtenir :

------ Direct Reference ------
Myself ID: 3735928559 (0xdeadbeef)
Myself Name: Malo
Myself Age: 19
Myself Address: 0x16b4fef40
------------------------------

------ Handle Reference ------
Myself ID: 3735928559 (0xdeadbeef)
Myself Name: Malo
Myself Age: 19
Myself Address: 0x16b4fef40
------------------------------
Enter fullscreen mode Exit fullscreen mode

Même si les adresses ne sont pas les mêmes que les miennes, elles doivent être identiques entre Direct Reference et Handle Reference.

Conclusion

Comprendre comment est gérée la mémoire en C est très important parce que vous pouvez faire ce que vous voulez. Ça peut sembler incroyable ce que nous venons de réaliser ici, pourtant il ne s'agit que d'une simple soustraction d'adresses.

J'espère que le contenu vous aura plus et que vous aurez appris quelque chose.

Top comments (0)