Attribut de nettoyage

Citation de la documentation GCC [1]:

L'attribut de nettoyage est utilisé pour exécuter une fonction lorsqu'une variable sort du domaine. Cet attribut ne peut être appliqué qu'aux variables automatiques et ne peut pas être utilisé avec des paramètres ou avec des variables statiques. La fonction doit prendre un paramètre, un pointeur vers un type compatible avec la variable. La valeur de retour de la fonction, le cas échéant, est ignorée.

Si l'option -fexceptions est activée, la fonction cleanup_function est lancée lorsque la pile est déroulée, pendant la gestion des exceptions. Notez que l'attribut de nettoyage n'intercepte pas les exceptions; il effectue uniquement une action. Si la fonction cleanup_function ne retourne pas normalement, le comportement n'est pas défini.




L'attribut de nettoyage est pris en charge par les compilateurs gcc et clang.

Dans cet article, je décrirai diverses options pour l'utilisation pratique de l'attribut de nettoyage et examinerai la structure interne de la bibliothèque, qui utilise le nettoyage pour implémenter les analogues std :: unique_ptr et std :: shared_ptr en C.

Essayons le nettoyage pour la désallocation de mémoire:

#include<stdlib.h>
#include<stdio.h>

static void free_int(int **ptr) 
{
    free(*ptr); 
    printf("cleanup done\n");
}

int main()
{
    __attribute__((cleanup(free_int))) int *ptr_one = (int *)malloc(sizeof(int));
    // do something here
    return 0;
}

Nous commençons, le programme affiche "nettoyage terminé". Tout fonctionne, bravo.

Mais un inconvénient apparaît immédiatement: on ne peut pas simplement écrire

__attribute__((cleanup(free_int)))

parce que la fonction appelée par l'attribut cleanup doit prendre un pointeur sur la variable libérée comme argument, et nous avons un pointeur sur la zone de mémoire allouée, c'est-à-dire que nous avons définitivement besoin d'une fonction qui prend un double pointeur. Pour ce faire, nous avons besoin d'une fonction wrapper supplémentaire:

static void free_int(int **ptr) 
{
    free(*ptr); 
    ...
}

De plus, nous ne pouvons pas utiliser une fonction universelle pour libérer des variables, car elles nécessiteront différents types d'arguments. Par conséquent, nous réécrivons la fonction comme suit:

static void _free(void *p) {
    free(*(void**) p);
    printf("cleanup done\n");  
}

Maintenant, elle peut accepter n'importe quel pointeur.

Voici une autre macro utile (à partir de la base de code systemd ):

#define DEFINE_TRIVIAL_CLEANUP_FUNC(type, func)                 \
        static inline void func##p(type *p) {                   \
                if (*p)                                         \
                        func(*p);                               \
        }                                                       \
        struct __useless_struct_to_allow_trailing_semicolon__

qui peut ensuite être utilisé comme ceci:

DEFINE_TRIVIAL_CLEANUP_FUNC(FILE*, pclose);
#define _cleanup_pclose_ __attribute__((cleanup(pclosep)))

Mais ce n'est pas tout. Il existe une bibliothèque qui implémente des analogues des avantages unique_ptr et shared_ptr en utilisant cet attribut: https://github.com/Snaipe/libcsptr

Exemple d'utilisation (tiré de [2]):

#include <stdio.h>
#include <csptr/smart_ptr.h>
#include <csptr/array.h>

void print_int(void *ptr, void *meta) {
    (void) meta;
    // ptr points to the current element
    // meta points to the array metadata (global to the array), if any.
    printf("%d\n", *(int*) ptr);
}

int main(void) {
    // Destructors for array types are run on every element of the
    // array before destruction.
    smart int *ints = unique_ptr(int[5], {5, 4, 3, 2, 1}, print_int);
    // ints == {5, 4, 3, 2, 1}

    // Smart arrays are length-aware
    for (size_t i = 0; i < array_length(ints); ++i) {
        ints[i] = i + 1;
    }
    // ints == {1, 2, 3, 4, 5}

    return 0;
}

Tout fonctionne à merveille!

Et voyons ce qu'il y a dans cette magie. Commençons par unique_ptr (et shared_ptr en même temps):

# define shared_ptr(Type, ...) smart_ptr(SHARED, Type, __VA_ARGS__)
# define unique_ptr(Type, ...) smart_ptr(UNIQUE, Type, __VA_ARGS__)

Allons de l'avant et voyons à quelle profondeur le terrier du lapin est:

# define smart_arr(Kind, Type, Length, ...)                                 \
    ({                                                                      \
        struct s_tmp {                                                      \
            CSPTR_SENTINEL_DEC                                              \
            __typeof__(__typeof__(Type)[Length]) value;                     \
            f_destructor dtor;                                              \
            struct {                                                        \
                const void *ptr;                                            \
                size_t size;                                                \
            } meta;                                                         \
        } args = {                                                          \
            CSPTR_SENTINEL                                                  \
            __VA_ARGS__                                                     \
        };                                                                  \
        void *var = smalloc(sizeof (Type), Length, Kind, ARGS_);            \
        if (var != NULL)                                                    \
            memcpy(var, &args.value, sizeof (Type));                        \
        var;                                                                \
    })

Jusqu'à présent, la clarté n'a pas augmenté, devant nous est un fouillis de macros dans les meilleures traditions de cette langue. Mais nous n'avons pas l'habitude de battre en retraite. Démêler l'enchevêtrement:

define CSPTR_SENTINEL        .sentinel_ = 0,
define CSPTR_SENTINEL_DEC int sentinel_;
...
typedef void (*f_destructor)(void *, void *);

Effectuez la substitution:

# define smart_arr(Kind, Type, Length, ...)                                 \
    ({                                                                      \
        struct s_tmp {                                                      \
            int sentinel_;                                                  \
            __typeof__(__typeof__(Type)[Length]) value;                     \
            void (*)(void *, void *) dtor;                                  \
            struct {                                                        \
                const void *ptr;                                            \
                size_t size;                                                \
            } meta;                                                         \
        } args = {                                                          \
            .sentinel_ = 0,                                                 \
            __VA_ARGS__                                                     \
        };                                                                  \
        void *var = smalloc(sizeof (Type), Length, Kind, ARGS_);            \
        if (var != NULL)                                                    \
            memcpy(var, &args.value, sizeof (Type));                        \
        var;                                                                \
    })

et essayez de comprendre ce qui se passe ici. Nous avons une certaine structure composée de la variable sentinelle_, un certain tableau (Type) [Longueur], un pointeur vers une fonction destructrice, qui est passé dans la partie supplémentaire (...) des arguments de macro, et une méta-structure, qui est également remplie d'arguments supplémentaires. Vient ensuite un appel

smalloc(sizeof (Type), Length, Kind, ARGS_);

Qu'est-ce que Smalloc? Nous trouvons un peu plus de magie de modèle (j'ai déjà fait quelques substitutions ici):

enum pointer_kind {
    UNIQUE,
    SHARED,
    ARRAY = 1 << 8
};
//..
typedef struct {
    CSPTR_SENTINEL_DEC
    size_t size;
    size_t nmemb;
    enum pointer_kind kind;
    f_destructor dtor;
    struct {
        const void *data;
        size_t size;
    } meta;
} s_smalloc_args;
//...
__attribute__ ((malloc)) void *smalloc(s_smalloc_args *args);
//...
#  define smalloc(...) \
    smalloc(&(s_smalloc_args) { CSPTR_SENTINEL __VA_ARGS__ })

Eh bien, c'est pourquoi nous aimons C. Il y a aussi de la documentation dans la bibliothèque (les saints, je recommande à tout le monde de prendre un exemple):

La fonction smalloc () appelle l'allocateur (malloc (3) par défaut), le pointeur retourné est un pointeur "intelligent". <...> Si la taille est 0, NULL est renvoyé. Si nmemb vaut 0, smalloc renverra un pointeur intelligent vers un bloc de mémoire d'au moins octets de taille et un pointeur scalaire intelligent, si nmemb n'est pas égal à 0, un pointeur vers un bloc de mémoire de taille au moins taille * nmemb est renvoyé et le pointeur est de type tableau.

original
«The smalloc() function calls an allocator (malloc (3) by default), such that the returned pointer is a smart pointer. <...> If size is 0, then smalloc() returns NULL. If nmemb is 0, then smalloc shall return a smart pointer to a memory block of at least size bytes, and the smart pointer is a scalar. Otherwise, it shall return a memory block to at least size * nmemb bytes, and the smart pointer is an array.»

Voici la source de smalloc:

__attribute__ ((malloc)) void *smalloc(s_smalloc_args *args) {
    return (args->nmemb == 0 ? smalloc_impl : smalloc_array)(args);
}

Regardons le code smalloc_impl, allouant des objets de types scalaires. Pour réduire le volume, j'ai supprimé le code associé aux pointeurs partagés et effectué la substitution en ligne et macro:

static void *smalloc_impl(s_smalloc_args *args) {
    if (!args->size)
        return NULL;

    // align the sizes to the size of a word
    size_t aligned_metasize = align(args->meta.size);
    size_t size = align(args->size);

    size_t head_size = sizeof (s_meta);
    s_meta_shared *ptr = malloc(head_size + size + aligned_metasize + sizeof (size_t));

    if (ptr == NULL)
        return NULL;

    char *shifted = (char *) ptr + head_size;
    if (args->meta.size && args->meta.data)
        memcpy(shifted, args->meta.data, args->meta.size);

    size_t *sz = (size_t *) (shifted + aligned_metasize);
    *sz = head_size + aligned_metasize;

    *(s_meta*) ptr = (s_meta) {
        .kind = args->kind,
        .dtor = args->dtor,
        .ptr = sz + 1
    };

    return sz + 1;
}

Nous voyons ici que la mémoire de la variable est allouée, plus un certain en-tête de type s_meta plus une zone de métadonnées de taille args-> meta.size alignée avec la taille du mot, plus un mot de plus (sizeof (size_t)). La fonction renvoie un pointeur sur la zone mémoire de la variable: ptr + head_size + aligné_metasize + 1. Allouons

une variable de type int, initialisée avec la valeur 42:

smart void *ptr = unique_ptr(int, 42);

Voici une macro intelligente:

# define smart __attribute__ ((cleanup(sfree_stack)))

Lorsque le pointeur quitte la portée, sfree_stack est appelé:

CSPTR_INLINE void sfree_stack(void *ptr) {
    union {
        void **real_ptr;
        void *ptr;
    } conv;
    conv.ptr = ptr;
    sfree(*conv.real_ptr);
    *conv.real_ptr = NULL;
}

Fonction Sfree (abrégée):

void sfree(void *ptr) {
    s_meta *meta = get_meta(ptr);
    dealloc_entry(meta, ptr);
}

La fonction dealloc_entry, en gros, appelle un destructeur personnalisé si nous l'avons spécifié dans les arguments unique_ptr, et que le pointeur vers celui-ci est stocké dans des métadonnées. Sinon, juste free (meta) est exécuté.

Liste des sources:

[1] Attributs de variable communs .
[2] Un bon moyen idiomatique d'utiliser GCC et de clanger __attribute __ ((nettoyage)) et les déclarations de pointeur .
[3] Utilisation de l'attribut de variable __cleanup__ dans GCC .

All Articles