Атрибут cleanup

    Цитата из документации GCC [1]:

    Атрибут cleanup предназначен для запуска функции, когда переменная выходит из области видимости. Этот атрибут может быть применён только к auto-переменным, и не может быть использован с параметрами или с static-переменными. Функция должна принимать один параметр, указатель на тип, совместимый с переменной. Возвращаемое значение функции, если оно есть, игнорируется.

    Если включена опция -fexceptions, то функция cleanup_function запускается при раскрутке стека, во время обработки исключения. Отметим, что атрибут cleanup не перехватывает исключения, он только выполняет действие. Если функция cleanup_function не выполняяет возврат нормальным образом, поведение не определено.




    Атрибут cleanup поддерживается компиляторами gcc и clang.

    В этой статье я приведу описание различных вариантов практического использования атрибута cleanup и рассмотрю внутреннее устройство библиотеки, которая использует cleanup для реализации аналогов std::unique_ptr и std::shared_ptr на языке C.

    Попробуем использовать cleanup для деаллокации памяти:

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

    Запускаем, программа печатает «cleanup done». Всё работает, ура.

    Но сразу становится очевиден один недостаток: мы не можем написать просто

    __attribute__((cleanup(free_int)))

    потому что функция, вызываемая атрибутом cleanup, должна принимать в качестве аргумента указатель на освобождаемую переменную, а у нас таковой является указатель на выделенную область памяти, то есть нам обязательно нужна функция, принимающая двойной указатель. Для этого нам нужна дополнительная функция-обёртка:

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

    К тому же, мы не можем использовать универсальную функцию для освобождения любых переменных, потому что они будут требовать разных типов аргументов. Поэтому перепишем функцию так:

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

    Теперь она может принимать любые указатели.

    Вот ещё полезный макрос (из кодовой базы 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__

    который в дальнейшем может использоваться так:

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

    Но это не всё. Есть библиотека, которая реализует аналоги плюсовых unique_ptr и shared_ptr с помощью этого атрибута: https://github.com/Snaipe/libcsptr

    Пример использования (взят из [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;
    }

    Всё чудесным образом работает!

    А давайте посмотрим, что внутри у этой магии. Начнём с unique_ptr (и заодно shared_ptr):

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

    Пойдём дальше, и посмотрим, насколько глубока кроличья нора:

    # 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;                                                                \
        })

    Пока что ясности не прибавилось, перед нами мешанина макросов в лучших традициях этого языка. Но мы не привыкли отступать. Распутываем клубок:

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

    Выполняем подстановку:

    # 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;                                                                \
        })

    и попытаемся понять, что тут происходит. У нас есть некая структура, состоящая из переменной sentinel_, некоего массива (Type)[Length], указателя на функцию-деструктор, который передаётся в дополнительной (...) части аргументов макроса, и структуры meta, которая также заполняется дополнительными аргументами. Далее происходит вызов

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

    Что такое smalloc? Находим ещё немного шаблонной магии (я уже выполнил здесь некоторые подстановки):

    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__ })

    Ну, за это мы и любим С. Также в библиотеке есть документация (святые люди, всем рекомендую брать с них пример):

    Функция smalloc() вызывает аллокатор (malloc (3) по умолчанию), возвращаемый указатель является «умным» указателем. <...> Если size равен 0, возвращается NULL. Если nmemb равен 0, то smalloc возвратит умный указатель на блок памяти, не менее size байт, и умный указатель скалярный, если nmemb не равен 0, возвращается указатель на блок памяти размера не менее size * nmemb, и указатель имеет тип array.

    оригинал
    «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.»

    Вот исходник smalloc:

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

    Посмотрим на код smalloc_impl, аллоцирующей объекты скалярных типов. Для сокращеня объёма я удалил код, связанный с shared-указателями, и сделал подстановку inline-ов и макросов:

    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;
    }

    Здесь мы видим, что аллоцируется память для переменной, плюс некий заголовок типа s_meta плюс область метаданных размера args->meta.size, выровненная по размеру слова, плюс ещё одно слово (sizeof(size_t)). Функция возвращает указатель на облась памяти переменной: ptr + head_size + aligned_metasize + 1.

    Пусть мы аллоцируем переменную типа int, инициализируемую значением 42:

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

    Здесь smart — это макрос:

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

    При выходе указателя из области видимости вызывается sfree_stack:

    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;
    }

    Функция sfree (с сокращениями):

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

    Функция dealloc_entry в оcновном, выполняет вызов кастомного деструктора, если мы его задавали в аргументах unique_ptr, и указатель на него сохранён в метаданных. Если его нет, выполняется просто free(meta).

    Список источников:

    [1] Common Variable Attributes.
    [2] A good and idiomatic way to use GCC and clang __attribute__((cleanup)) and pointer declarations.
    [3] Using the __cleanup__ variable attribute in GCC.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 5

      0

      Хороший пример использования этого атрибута – critical section в атмеловской либе для atmega/attiny.

        +1
        С __attribbute__(( cleanup(...) )) могут быть нюансы, связанные с повторным заходом в область видимости.
        Пример:
        int main(void)
        {
          {
            struct Obj *test __attribute__(( cleanup(Obj_cleanup) )) = calloc(1, sizeof(struct Obj));
            fprintf(stdout, "Obj allocated at %p\n", (void*)test);
            
            fprintf(stdout, "Quitting the test scope...\n");
            goto main__sub1;
          
        main__exit:
            return 0;
          } // <-- Obj_cleanup is executed here twice, even though the control flow passes the 'test' definition only once
        
        main__sub1:
          fprintf(stdout, "The test scope has been quit.\n");
          goto main__exit;
        }

        Вывод:
        Obj allocated at 00000000005F6880
        Quitting the test scope…
        Obj_cleanup() for 00000000005F6880
        The test scope has been quit.
        Obj_cleanup() for 00000000005F6880

        А вот clang отслеживает наличие на границе области видимости выполняющейся cleanup-функции, зависящей от переменной test, и отказывается компилировать такой код.
          0
          Интересно, спасибо.
          0
          int *ptr_one = (int *)malloc(sizeof(int));

          1. Не стоит преобразовывать явно возвращаемое значение malloc — это Си, а не Си++, и здесь void * приводится к любому указателю: void * для этого и придуман.
          2. Не стоит в данной конструкции использовать sizeof(int), конструкция sizeof(*ptr_one) уменьшает число точек отказа при поддержке кода.

          Например, при смене типа переменной (int * на long * — как вариант), если выполнить пункты выше — вся конструкция останется валидной. (А лучше, если здесь будет указатель на структурный объект.)


          Если оставить как есть, то рано или поздно кто-нибудь да забудет исправить размер выделяемой памяти, а явное преобразование типа запретит умному компилятору выдать предупреждение об этом (так как явное преобразование — это «делай как я сказал» в Си).

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое