Obtenir une structure depuis un membre en C
Published: 2023-10-27 09:40:39

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 :

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

Ainsi que cette instance :
```c
Person myself = {
    .id = (unsigned long long) 0xdeadbeef,
    .name = "Malo",
    .age = 19,
    .get_info = get_info
};
```

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

```c
Person *get_person_handle_by_name(char const **name);
```

`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 :
```c
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
*/
```

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 :

```c
Person myself = {
    .id = (unsigned long long) 0xdeadbeef,
    .name = "Malo",
    .age = 19,
    .get_info = get_info
};
```

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

```c
printf("&myself : %p\n", (void *) &myself);
printf("&myself.id : %p\n", (void *) &myself.id);
/*
Sortie :
&myself : 0x16fb96f40
&myself.id : 0x16fb96f40
*/
```

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 :

```c
printf("Ox%llx\n", *((unsigned long long *) (&myself)));
/*
Sortie :
0xdeadbeef
*/
```

Ou encore ça :
```c
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
*/
```

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 :

```c
Person *get_person_handle_by_name(char const **name);
```

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.

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

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

```c
#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;
}
```

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
------------------------------
```

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.