سمة التنظيف

اقتباس من وثائق مجلس التعاون الخليجي [1]:

تُستخدم سمة التنظيف لتشغيل دالة عندما يخرج متغير عن نطاقه. لا يمكن تطبيق هذه السمة إلا على المتغيرات التلقائية ، ولا يمكن استخدامها مع المعلمات أو المتغيرات الثابتة. يجب أن تأخذ الدالة معلمة واحدة ، مؤشر إلى نوع متوافق مع المتغير. يتم تجاهل القيمة المرجعة للدالة ، إن وجدت.

إذا تم تمكين خيار -fexceptions ، فسيتم تشغيل وظيفة cleanup_function عندما يتم فك المكدس ، أثناء معالجة الاستثناء. لاحظ أن سمة التنظيف لا تلتقط الاستثناءات ؛ فهي تقوم بإجراء فقط. إذا لم تُرجع الدالة cleanup_function بشكل طبيعي ، فسيكون السلوك غير محدد.




يتم دعم سمة التنظيف من قبل جامعي gcc و clang.

في هذه المقالة سأصف الخيارات المختلفة للاستخدام العملي لخاصية التنظيف وأخذ بعين الاعتبار الهيكل الداخلي للمكتبة ، والذي يستخدم التنظيف لتنفيذ نظائر std :: unique_ptr و std :: shared_ptr في C.

دعنا نحاول تنظيف تخصيص الذاكرة:

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

نبدأ ، يقوم البرنامج بطباعة "تنظيف". كل شيء يعمل ، هتاف.

لكن هناك عيب واحد يتضح على الفور: لا يمكننا ببساطة الكتابة

__attribute__((cleanup(free_int)))

لأن الوظيفة التي تسمى بسمة التنظيف يجب أن تأخذ مؤشرًا إلى المتغير المحرر كوسيطة ، ولدينا مؤشر إلى منطقة الذاكرة المخصصة ، أي أننا بحاجة بالتأكيد إلى وظيفة تأخذ مؤشرًا مزدوجًا. للقيام بذلك ، نحتاج إلى وظيفة غلاف إضافية:

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_ ، صفيف معين (النوع) [الطول] ، مؤشر إلى وظيفة مدمرة ، والتي يتم تمريرها في الجزء الإضافي (...) من الوسائط الماكرو ، وهيكل ميتا ، والذي يتم ملؤه أيضًا بوسيطات إضافية. التالي هو مكالمة

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

ما هو سمولوك؟ نجد المزيد من سحر القوالب (لقد قمت بالفعل ببعض الاستبدالات هنا):

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

حسنًا ، لهذا السبب نحب C. هناك أيضًا وثائق في المكتبة ( أيها الأولياء ، أوصي الجميع بأخذ مثال منهم): تستدعي

الدالة smalloc () المُخصص (malloc (3) افتراضيًا) ، المؤشر الذي تم إرجاعه هو مؤشر "ذكي". <...> إذا كان الحجم 0 ، يتم إرجاع NULL. إذا كان nmemb يساوي 0 ، فسوف يعيد smalloc مؤشرًا ذكيًا إلى كتلة ذاكرة بحجم بايت على الأقل ، ومؤشر عددي ذكي ، إذا لم يكن nmemb يساوي 0 ، فسيتم إرجاع مؤشر إلى كتلة ذاكرة بحجم على الأقل * nmemb ، وكان المؤشر من نوع الصفيف.

أصلي
«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 ، الذي يخصص كائنات من الأنواع العددية. لتقليل حجم الصوت ، قمت بحذف الرمز المرتبط بالمؤشرات المشتركة وقمت باستبدال ماكرو وماكرو:

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 + align_metasize + 1.

دعنا نخصص متغيرًا من النوع int ، مُهيأًا بالقيمة 42:

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

هنا ذكي ماكرو:

# 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 ، بشكل أساسي ، باستدعاء أداة إتلاف مخصصة إذا حددناها في وسائط unique_ptr ، وتم تخزين المؤشر إليها في بيانات التعريف. إذا لم يكن الأمر كذلك ، يتم فقط تنفيذ (meta) مجانًا.

قائمة المصادر:

[1] السمات المتغيرة المشتركة .
[2] طريقة جيدة واصطلاحية لاستخدام GCC و clang __attribute __ ((تنظيف)) وإعلانات المؤشر .
[3] استخدام سمة المتغير __cleanup__ في دول مجلس التعاون الخليجي .

All Articles