Bereinigungsattribut

Zitat aus der GCC-Dokumentation [1]:

Das Bereinigungsattribut wird verwendet, um eine Funktion auszuführen, wenn eine Variable den Gültigkeitsbereich verlässt. Dieses Attribut kann nur auf automatische Variablen angewendet werden und kann nicht mit Parametern oder statischen Variablen verwendet werden. Die Funktion muss einen Parameter annehmen, einen Zeiger auf einen mit der Variablen kompatiblen Typ. Der Rückgabewert der Funktion, falls vorhanden, wird ignoriert.

Wenn die Option -fexceptions aktiviert ist, wird die Funktion cleanup_function gestartet, wenn der Stapel abgewickelt wird, während die Ausnahme verarbeitet wird. Beachten Sie, dass das Bereinigungsattribut keine Ausnahmen abfängt, sondern nur eine Aktion ausführt. Wenn die Bereinigungsfunktion nicht normal zurückkehrt, ist das Verhalten undefiniert.




Das Bereinigungsattribut wird von den Compilern gcc und clang unterstützt.

In diesem Artikel werde ich verschiedene Optionen für die praktische Verwendung des Bereinigungsattributs beschreiben und die interne Struktur der Bibliothek berücksichtigen, die die Bereinigung verwendet, um die Analoga std :: unique_ptr und std :: shared_ptr in C zu implementieren.

Versuchen wir, die Speicherfreigabe zu bereinigen:

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

Wir starten, das Programm druckt "Bereinigung erledigt". Alles funktioniert, Prost.

Ein Nachteil wird jedoch sofort deutlich: Wir können nicht einfach schreiben

__attribute__((cleanup(free_int)))

Da die vom Bereinigungsattribut aufgerufene Funktion einen Zeiger auf die freigegebene Variable als Argument verwenden muss und wir einen Zeiger auf den zugewiesenen Speicherbereich haben, benötigen wir definitiv eine Funktion, die einen Doppelzeiger verwendet. Dazu benötigen wir eine zusätzliche Wrapper-Funktion:

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

Darüber hinaus können wir keine universelle Funktion verwenden, um Variablen freizugeben, da für sie unterschiedliche Arten von Argumenten erforderlich sind. Daher schreiben wir die Funktion wie folgt um:

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

Jetzt kann sie alle Hinweise akzeptieren.

Hier ist ein weiteres nützliches Makro (aus der systemd- Codebasis ):

#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__

die später so verwendet werden kann:

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

Aber das ist nicht alles. Es gibt eine Bibliothek, die Analoga der Pluszeichen unique_ptr und shared_ptr mithilfe dieses Attributs implementiert: https://github.com/Snaipe/libcsptr

Verwendungsbeispiel (entnommen aus [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;
}

Alles funktioniert wunderbar!

Und mal sehen, was in dieser Magie steckt. Beginnen wir mit unique_ptr (und shared_ptr gleichzeitig):

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

Lassen Sie uns sehen, wie tief das Kaninchenloch ist:

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

Bisher hat die Klarheit nicht zugenommen, bevor wir ein Durcheinander von Makros in den besten Traditionen dieser Sprache haben. Aber wir sind es nicht gewohnt, uns zurückzuziehen. Löse das Gewirr:

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

Führen Sie die Ersetzung durch:

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

und versuchen zu verstehen, was hier passiert. Wir haben eine bestimmte Struktur, bestehend aus der Variablen sentinel_, einem bestimmten Array (Typ) [Länge], einem Zeiger auf eine Destruktorfunktion, die im zusätzlichen (...) Teil der Makroargumente übergeben wird, und einer Metastruktur, die ebenfalls mit zusätzlichen Argumenten gefüllt ist. Als nächstes kommt ein Anruf

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

Was ist Smalloc? Wir finden etwas mehr Vorlagenmagie (ich habe hier bereits einige Ersetzungen vorgenommen):

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

Deshalb lieben wir C. Es gibt auch Dokumentation in der Bibliothek (Heilige, ich empfehle jedem, ein Beispiel daraus zu ziehen):

Die Funktion smalloc () ruft den Allokator ( standardmäßig malloc (3)) auf, der zurückgegebene Zeiger ist ein „intelligenter“ Zeiger. <...> Wenn die Größe 0 ist, wird NULL zurückgegeben. Wenn nmemb 0 ist, gibt smalloc einen intelligenten Zeiger auf einen Speicherblock mit mindestens Bytes und einen intelligenten Skalarzeiger zurück. Wenn nmemb nicht gleich 0 ist, wird ein Zeiger auf einen Speicherblock mit mindestens Größe * nmemb zurückgegeben, und der Zeiger ist vom Typ 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.»

Hier ist die Quelle von smalloc:

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

Schauen wir uns den Code smalloc_impl an, der Objekte skalaren Typs zuweist. Um die Lautstärke zu verringern, habe ich den mit gemeinsam genutzten Zeigern verknüpften Code gelöscht und Inline- und Makrosubstitutionen vorgenommen:

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

Hier sehen wir, dass der Speicher für die Variable zugewiesen wird, plus einem bestimmten Header vom Typ s_meta plus einem Metadatenbereich der Größe args-> meta.size, der an der Größe des Wortes ausgerichtet ist, plus einem weiteren Wort (sizeof (size_t)). Die Funktion gibt einen Zeiger auf den Speicher der Variablen zurück: ptr + head_size + align_metasize + 1.

Ordnen Sie eine Variable vom Typ int zu, die mit dem Wert 42 initialisiert ist:

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

Hier ist smart ein Makro:

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

Wenn der Zeiger den Gültigkeitsbereich verlässt, wird sfree_stack aufgerufen:

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-Funktion (abgekürzt):

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

Die Funktion dealloc_entry ruft im Grunde genommen einen benutzerdefinierten Destruktor auf, wenn wir ihn in den Argumenten unique_ptr angegeben haben und der Zeiger darauf in den Metadaten gespeichert ist. Wenn nicht, wird nur free (meta) ausgeführt.

Liste der Quellen:

[1] Allgemeine Variablenattribute .
[2] Eine gute und idiomatische Möglichkeit, GCC und Clang __attribute __ ((Bereinigung)) und Zeigerdeklarationen zu verwenden .
[3] Verwenden des Variablenattributs __cleanup__ in GCC .

All Articles