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