НОВОСТИ Атрибут cleanup

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

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


qidrgzjppagqdyrqn7c4ykxa2a4.jpeg


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

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

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


#include
#include

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

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

Вот ещё полезный макрос (из кодовой базы ):

#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 с помощью этого атрибута:

Пример использования (взят из [2]):

#include
#include smart_ptr.h>
#include 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 + 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 /..
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] .
[2] .
[3] .
 
Сверху Снизу