Quote from the GCC documentation [1]:The cleanup attribute is used to run a function when a variable goes out of scope. This attribute can only be applied to auto variables, and cannot be used with parameters or with static variables. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function, if any, is ignored.
If the -fexceptions option is enabled, then the cleanup_function function is launched when the stack is unwound, while the exception is being processed. Note that the cleanup attribute does not catch exceptions; it only performs an action. If the cleanup_function does not return normally, the behavior is undefined.
The cleanup attribute is supported by the gcc and clang compilers.In this article I will describe various options for the practical use of the cleanup attribute and consider the internal structure of the library, which uses cleanup to implement the std :: unique_ptr and std :: shared_ptr analogs in C.Let's try cleanup for memory deallocation:#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));
return 0;
}
We start, the program prints "cleanup done". Everything works, cheers.But one drawback immediately becomes apparent: we cannot simply write__attribute__((cleanup(free_int)))
because the function called by the cleanup attribute must take a pointer to the freed variable as an argument, and we have a pointer to the allocated memory area, that is, we definitely need a function that takes a double pointer. To do this, we need an additional wrapper function:static void free_int(int **ptr)
{
free(*ptr);
...
}
In addition, we cannot use a universal function to free any variables, because they will require different types of arguments. Therefore, we rewrite the function as follows:static void _free(void *p) {
free(*(void**) p);
printf("cleanup done\n");
}
Now she can accept any pointers.Here is another useful macro (from the systemd code base ):#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__
which can later be used like this:DEFINE_TRIVIAL_CLEANUP_FUNC(FILE*, pclose);
#define _cleanup_pclose_ __attribute__((cleanup(pclosep)))
But that's not all. There is a library that implements analogues of the unique_ptr and shared_ptr pluses using this attribute: https://github.com/Snaipe/libcsptrExample of use (taken from [2]):#include <stdio.h>
#include <csptr/smart_ptr.h>
#include <csptr/array.h>
void print_int(void *ptr, void *meta) {
(void) meta;
printf("%d\n", *(int*) ptr);
}
int main(void) {
smart int *ints = unique_ptr(int[5], {5, 4, 3, 2, 1}, print_int);
for (size_t i = 0; i < array_length(ints); ++i) {
ints[i] = i + 1;
}
return 0;
}
Everything works wonderfully!And let's see what's inside this magic. Let's start with unique_ptr (and shared_ptr at the same time):# define shared_ptr(Type, ...) smart_ptr(SHARED, Type, __VA_ARGS__)
# define unique_ptr(Type, ...) smart_ptr(UNIQUE, Type, __VA_ARGS__)
Let's go ahead and see how deep the rabbit hole is:# 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; \
})
So far, clarity has not increased, before us is a jumble of macros in the best traditions of this language. But we are not used to retreat. Unravel the tangle:define CSPTR_SENTINEL .sentinel_ = 0,
define CSPTR_SENTINEL_DEC int sentinel_;
...
typedef void (*f_destructor)(void *, void *);
Perform the substitution:# 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; \
})
and try to understand what is happening here. We have a certain structure, consisting of the sentinel_ variable, a certain array (Type) [Length], a pointer to a destructor function, which is passed in the additional (...) part of the macro arguments, and a meta structure, which is also filled with additional arguments. Next is a callsmalloc(sizeof (Type), Length, Kind, ARGS_);
What is smalloc? We find some more template magic (I have already done some substitutions here):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__ })
Well, that’s why we love C. There is also documentation in the library (holy people, I recommend everyone to take an example from them):The smalloc () function calls the allocator (malloc (3) by default), the returned pointer is a “smart” pointer. <...> If size is 0, NULL is returned. If nmemb is 0, then smalloc will return a smart pointer to a memory block of at least size bytes, and a smart scalar pointer, if nmemb is not equal to 0, a pointer to a memory block of size at least size * nmemb is returned, and the pointer is of type array.original«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.»
Here is the source of smalloc:__attribute__ ((malloc)) void *smalloc(s_smalloc_args *args) {
return (args->nmemb == 0 ? smalloc_impl : smalloc_array)(args);
}
Let's look at the code smalloc_impl, allocating objects of scalar types. To reduce the volume, I deleted the code associated with shared pointers and made inline and macro substitution:static void *smalloc_impl(s_smalloc_args *args) {
if (!args->size)
return NULL;
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;
}
Here we see that the memory for the variable is allocated, plus a certain header of type s_meta plus a metadata area of size args-> meta.size aligned with the size of the word, plus one more word (sizeof (size_t)). The function returns a pointer to the memory area of the variable: ptr + head_size + aligned_metasize + 1.Let us allocate a variable of type int, initialized with value 42:smart void *ptr = unique_ptr(int, 42);
Here smart is a macro:# define smart __attribute__ ((cleanup(sfree_stack)))
When the pointer leaves the scope, sfree_stack is called: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 function (abbreviated):void sfree(void *ptr) {
s_meta *meta = get_meta(ptr);
dealloc_entry(meta, ptr);
}
The dealloc_entry function, basically, calls a custom destructor if we specified it in the unique_ptr arguments, and the pointer to it is stored in metadata. If not, just free (meta) is executed.List of sources:[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 .