Atribut pembersihan

Kutipan dari dokumentasi GCC [1]:

Atribut cleanup digunakan untuk menjalankan fungsi ketika sebuah variabel keluar dari cakupan. Atribut ini hanya dapat diterapkan ke variabel otomatis, dan tidak dapat digunakan dengan parameter atau dengan variabel statis. Fungsi harus mengambil satu parameter, pointer ke tipe yang kompatibel dengan variabel. Nilai pengembalian fungsi, jika ada, diabaikan.

Jika opsi -fusionions diaktifkan, maka fungsi cleanup_function diluncurkan ketika stack dibatalkan, selama penanganan pengecualian. Perhatikan bahwa atribut pembersihan tidak menangkap pengecualian, tetapi hanya melakukan tindakan. Jika fungsi cleanup_fungsi tidak kembali secara normal, perilaku tidak terdefinisi.




Atribut cleanup didukung oleh kompiler gcc dan dentang.

Dalam artikel ini saya akan menjelaskan berbagai opsi untuk penggunaan praktis atribut pembersihan dan mempertimbangkan struktur internal perpustakaan, yang menggunakan pembersihan untuk menerapkan analog std :: unique_ptr dan std :: shared_ptr di C.

Mari kita coba pembersihan untuk alokasi memori:

#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;
}

Kami mulai, program mencetak "pembersihan selesai". Semuanya bekerja, tepuk tangan.

Tetapi satu kelemahan segera menjadi jelas: kita tidak bisa begitu saja menulis

__attribute__((cleanup(free_int)))

karena fungsi yang dipanggil oleh atribut cleanup harus mengambil pointer ke variabel yang dibebaskan sebagai argumen, dan kita memiliki pointer ke area memori yang dialokasikan, yaitu, kita pasti membutuhkan fungsi yang mengambil pointer ganda. Untuk melakukan ini, kita memerlukan fungsi wrapper tambahan:

static void free_int(int **ptr) 
{
    free(*ptr); 
    ...
}

Selain itu, kami tidak dapat menggunakan fungsi universal untuk membebaskan variabel apa pun, karena mereka akan membutuhkan berbagai jenis argumen. Karena itu, kami menulis ulang fungsinya sebagai berikut:

static void _free(void *p) {
    free(*(void**) p);
    printf("cleanup done\n");  
}

Sekarang dia bisa menerima petunjuk apa pun.

Berikut ini adalah makro lain yang bermanfaat (dari basis kode 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__

yang nantinya bisa digunakan seperti ini:

DEFINE_TRIVIAL_CLEANUP_FUNC(FILE*, pclose);
#define _cleanup_pclose_ __attribute__((cleanup(pclosep)))

Tapi itu belum semuanya. Ada perpustakaan yang mengimplementasikan analog dari unique_ptr dan shared_ptr plus menggunakan atribut ini: https://github.com/Snaipe/libcsptr

Contoh penggunaan (diambil dari [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;
}

Semuanya bekerja dengan luar biasa!

Dan mari kita lihat apa yang ada di dalam sihir ini. Mari kita mulai dengan unique_ptr (dan shared_ptr secara bersamaan):

# define shared_ptr(Type, ...) smart_ptr(SHARED, Type, __VA_ARGS__)
# define unique_ptr(Type, ...) smart_ptr(UNIQUE, Type, __VA_ARGS__)

Mari kita lanjutkan dan lihat seberapa dalam lubang kelinci itu:

# 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;                                                                \
    })

Sejauh ini, kejelasan belum meningkat, sebelum kita adalah tumpukan makro dalam tradisi terbaik bahasa ini. Tapi kita tidak terbiasa mundur. Mengurai kusut:

define CSPTR_SENTINEL        .sentinel_ = 0,
define CSPTR_SENTINEL_DEC int sentinel_;
...
typedef void (*f_destructor)(void *, void *);

Lakukan penggantian:

# 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;                                                                \
    })

dan mencoba memahami apa yang terjadi di sini. Kami memiliki struktur tertentu, yang terdiri dari variabel sentinel_, array tertentu (Tipe) [Panjang], pointer ke fungsi destruktor, yang diteruskan di bagian tambahan (...) dari argumen makro, dan struktur meta, yang juga diisi dengan argumen tambahan. Berikutnya adalah panggilan

smalloc(sizeof (Type), Length, Kind, ARGS_);

Apa itu smalloc? Kami menemukan beberapa keajaiban templat lagi (saya sudah melakukan beberapa pergantian di sini):

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__ })

Nah, itu sebabnya kami mencintai C. Ada juga dokumentasi di perpustakaan (orang suci, saya sarankan semua orang untuk mengambil contoh dari mereka):

Fungsi smalloc () memanggil pengalokasi (malloc (3) secara default), pointer yang dikembalikan adalah pointer "pintar". <...> Jika ukurannya 0, NULL dikembalikan. Jika nmemb adalah 0, maka smalloc akan mengembalikan pointer pintar ke blok memori setidaknya ukuran byte, dan pointer skalar pintar, jika nmemb tidak sama dengan 0, pointer ke blok memori ukuran setidaknya ukuran * nmemb dikembalikan, dan pointer adalah tipe array.

asli
Β«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.Β»

Berikut adalah sumber smalloc:

__attribute__ ((malloc)) void *smalloc(s_smalloc_args *args) {
    return (args->nmemb == 0 ? smalloc_impl : smalloc_array)(args);
}

Mari kita lihat kode smalloc_impl, mengalokasikan objek tipe skalar. Untuk mengurangi volume, saya menghapus kode yang terkait dengan pointer bersama dan membuat penggantian inline dan makro:

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

Di sini kita melihat bahwa memori untuk variabel dialokasikan, ditambah header tipe s_meta tertentu ditambah area metadata ukuran args-> meta.size selaras dengan ukuran kata, ditambah satu kata lagi (sizeof (size_t)). Fungsi mengembalikan pointer ke area memori variabel: ptr + head_size + aligned_metasize + 1.

Mari kita mengalokasikan variabel tipe int, diinisialisasi dengan nilai 42:

smart void *ptr = unique_ptr(int, 42);

Di sini pintar adalah makro:

# define smart __attribute__ ((cleanup(sfree_stack)))

Ketika pointer meninggalkan ruang lingkup, sfree_stack disebut:

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

Fungsi bebas (disingkat):

void sfree(void *ptr) {
    s_meta *meta = get_meta(ptr);
    dealloc_entry(meta, ptr);
}

Fungsi dealloc_entry, pada dasarnya, memanggil destruktor kustom jika kita menentukannya dalam argumen unique_ptr, dan penunjuknya disimpan dalam metadata. Jika tidak, hanya gratis (meta) yang dieksekusi.

Daftar sumber:

[1] Atribut Variabel Umum .
[2] Cara yang baik dan idiomatis untuk menggunakan GCC dan membunyikan __attribute __ ((pembersihan)) dan deklarasi pointer .
[3] Menggunakan atribut variabel __cleanup__ dalam GCC .

All Articles