Цитата из документации GCC [1]:
Атрибут cleanup предназначен для запуска функции, когда переменная выходит из области видимости. Этот атрибут может быть применён только к auto-переменным, и не может быть использован с параметрами или с static-переменными. Функция должна принимать один параметр, указатель на тип, совместимый с переменной. Возвращаемое значение функции, если оно есть, игнорируется.
Если включена опция -fexceptions, то функция cleanup_function запускается при раскрутке стека, во время обработки исключения. Отметим, что атрибут cleanup не перехватывает исключения, он только выполняет действие. Если функция cleanup_function не выполняяет возврат нормальным образом, поведение не определено.

Атрибут cleanup поддерживается компиляторами gcc и clang.
В этой статье я приведу описание различных вариантов практического использования атрибута cleanup и рассмотрю внутреннее устройство библиотеки, которая использует cleanup для реализации аналогов std::unique_ptr и std::shared_ptr на языке C.
Попробуем использовать cleanup для деаллокации памяти:
Запускаем, программа печатает «cleanup done». Всё работает, ура.
Но сразу становится очевиден один недостаток: мы не можем написать просто
потому что функция, вызываемая атрибутом cleanup, должна принимать в качестве аргумента указатель на освобождаемую переменную, а у нас таковой является указатель на выделенную область памяти, то есть нам обязательно нужна функция, принимающая двойной указатель. Для этого нам нужна дополнительная функция-обёртка:
К тому же, мы не можем использовать универсальную функцию для освобождения любых переменных, потому что они будут требовать разных типов аргументов. Поэтому перепишем функцию так:
Теперь она может принимать любые указатели.
Вот ещё полезный макрос (из кодовой базы systemd):
который в дальнейшем может использоваться так:
Но это не всё. Есть библиотека, которая реализует аналоги плюсовых unique_ptr и shared_ptr с помощью этого атрибута: https://github.com/Snaipe/libcsptr
Пример использования (взят из [2]):
Всё чудесным образом работает!
А давайте посмотрим, что внутри у этой магии. Начнём с unique_ptr (и заодно shared_ptr):
Пойдём дальше, и посмотрим, насколько глубока кроличья нора:
Пока что ясности не прибавилось, перед нами мешанина макросов в лучших традициях этого языка. Но мы не привыкли отступать. Распутываем клубок:
Выполняем подстановку:
и попытаемся понять, что тут происходит. У нас есть некая структура, состоящая из переменной sentinel_, некоего массива (Type)[Length], указателя на функцию-деструктор, который передаётся в дополнительной (...) части аргументов макроса, и структуры meta, которая также заполняется дополнительными аргументами. Далее происходит вызов
Что такое smalloc? Находим ещё немного шаблонной магии (я уже выполнил здесь некоторые подстановки):
Ну, за это мы и любим С. Также в библиотеке есть документация (святые люди, всем рекомендую брать с них пример):
Функция smalloc() вызывает аллокатор (malloc (3) по умолчанию), возвращаемый указатель является «умным» указателем. <...> Если size равен 0, возвращается NULL. Если nmemb равен 0, то smalloc возвратит умный указатель на блок памяти, не менее size байт, и умный указатель скалярный, если nmemb не равен 0, возвращается указатель на блок памяти размера не менее size * nmemb, и указатель имеет тип array.
Вот исходник smalloc:
Посмотрим на код smalloc_impl, аллоцирующей объекты скалярных типов. Для сокращеня объёма я удалил код, связанный с shared-указателями, и сделал подстановку inline-ов и макросов:
Здесь мы видим, что аллоцируется память для переменной, плюс некий заголовок типа s_meta плюс область метаданных размера args->meta.size, выровненная по размеру слова, плюс ещё одно слово (sizeof(size_t)). Функция возвращает указатель на облась памяти переменной: ptr + head_size + aligned_metasize + 1.
Пусть мы аллоцируем переменную типа int, инициализируемую значением 42:
Здесь smart — это макрос:
При выходе указателя из области видимости вызывается sfree_stack:
Функция sfree (с сокращениями):
Функция 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.
Атрибут 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.
