Атрибут 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.

ссылка на оригинал статьи https://habr.com/ru/post/503536/