Как стать автором
Обновить

Добавляем деструкторы в C

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров5.3K

В данной статье будет описано создание кастомного аллокатора на си c регистрацией колбеков, которые будут вызваны при освобождении памяти. Нужен для того, чтобы при создании записать туда деструктор, а в конце просто вызвать free, не погружаясь в детали его работы.

Основная идея

Задумка весьма простая — аллокатор кастомный, так давайте просто выделим немного больше памяти, в её начало положим callback‑информацию и вернём смещённый указатель. Диаграммой выглядит это всё примерно так:

В callback данных просто‑напросто лежит указатель на массив callback‑функций, его длина и ёмкость (capacity).

Функция callback_free извлекает из callback‑данных информацию про функции, по очереди их вызывает и после передаёт весь кусок функции free.

Реализация

Как всегда, начнём с объявлений типов:

typedef void (*callback_fn)(void *addr, void *res);

typedef struct {
    void *resource;
    callback_fn fn;
} callback_t;

callback может использовать какие‑то дополнительные значения, поэтому их передаём через указатель resource. callback_t получается неким аналогом замыкания, но в сишном стиле.

Теперь объявляем структуру chunk_t :

typedef struct {
    size_t capacity;
    size_t length;
    callback_t *callbacks;
    alignas(max_align_t) char memory[];
} chunk_t;
Зачем там alignas?

Стандарнтый аллокатор в си возвращает указатель на участок памяти с выравниванием соответствующим max_align_t, то есть таким, что оно годится для любого стандартного типа.

Компилятор же волен упаковать нашу структуру произвольным образом, а это означает, что необходимое выравнивание может потеряться. Чтобы этого не произошло явно указываем, что участок memory выровнян так же как max_align_t

В будущем часто понадобится сдвигать указатель от начала структуры к memory и в обратную сторону, поэтому добавляем две вспомогательные функции

static inline void* chunk_to_ptr(chunk_t *chunk) {
    char *_ptr = (char *) chunk;
    // Не использую &chunk->memory, чтобы сохранить единообразность кода
    return _ptr + offsetof(chunk_t, memory);  
}

static inline void* ptr_to_chunk(void *ptr) {
    char *_ptr = (char *) ptr;
    return _ptr - offsetof(chunk_t, memory);
}

Теперь определить callback_alloc и callback_free не составляет никакого труда:

void* callback_alloc(size_t size) {
    chunk_t *chunk = malloc(sizeof(chunk_t) + size);
    // Инициализировали массив callback-ов
    chunk->capacity = 0;
    chunk->length = 0;
    chunk->callbacks = nullptr;

    return chunk_to_ptr(chunk);
}

void callback_free(void *ptr) {
    // Стандартный free умеет принимать nullptr, дублируем его поведение
    if(! ptr)
        return;

    chunk_t *chunk = ptr_to_chunk(ptr);
    
    for(int i = 0; i < chunk->length; i ++) {
        // Достаём функцию и доп.данные из массива
        callback_fn fn = chunk->callbacks[i].fn;
        void *resource = chunk->callbacks[i].resource;
        // Вызываем callback
        fn(ptr, resource);
    }
    // Освободили массив callback-ов
    free(chunk->callbacks);
    // Освободили занятую память
    free(ptr_to_chunk(ptr));
}

И основная функциональность — функция add_callback

void add_callback(void *ptr, callback_t callback) {
    if(! ptr)
        return;

    chunk_t *chunk = ptr_to_chunk(ptr);

    if(! chunk->callbacks) {
        // Инициализируем список, если пустой
        // (memcpy не работает с пустыми указателями, 
        // поэтому обрабатываем отдельно)
        chunk->capacity = 10;
        chunk->length = 0;
        chunk->callbacks = malloc(10 * sizeof(callback_t));
    }

    if(chunk->length >= chunk->capacity) {
        // Переаллокация, если заполнили массив
        size_t capacity = chunk->capacity;
        callback_t *old_callbacks = chunk->callbacks;
        callback_t *new_callbacks = malloc((capacity + 10) * sizeof(callback_t));
        
        memcpy(new_callbacks, old_callbacks, capacity * sizeof(callback_t));
        
        free(old_callbacks);
        
        chunk->callbacks = new_callbacks;
        chunk->capacity += 10;
    }
    // Добавили в конец новый callback
    chunk->callbacks[chunk->length ++] = callback;
}

Примеры использования

Двумерный массив

Начнём с банального — двумерный динамический массив, но теперь не нужно в конце писать цикл с free для освобождения одномерных подмассивов.

Для этого создаём функцию-конструктор array2d_ctor, куда передаём размеры по x, y и размер элемента.

void* array2d_ctor(size_t size_y, size_t size_x, size_t elem_size) {
    // Создаём как обычно массив
    void **array2d = callback_alloc(size_y * sizeof(void *));
    
    for(int i = 0; i < size_y; i ++)
        array2d[i] = callback_alloc(size_x * elem_size);

    // Прокидываем размер массива в деструктор, чтобы там знать сколько
    // итераций нужно делать в цикле
    size_t *arr_data = malloc(sizeof(size_t));
    *arr_data = size_y;
    
    add_callback(array2d, (callback_t) {
            .resource = arr_data,
            .fn = array2d_dtor
        });
    
    return array2d;
}

Ну и без деструктора ничего не заведётся, поэтому добавляем и его:

void array2d_dtor(void *ptr, void *resource) {
    void **array2d = ptr;
    // Излекаем размер массива и чистим resource
    size_t size_y = *(size_t *)resource;
    free(resource);

    // Освобождаем память занятую подмассивами
    for(int i = 0; i < size_y; i ++)
        callback_free(array2d[i]);
}

Создадим двумерный массив с табличкой умножения, выведем её и освободим массив:

int main() {
    int **matrix = array2d_ctor(10, 10, sizeof(int));

    for(int i = 0; i < 10; i ++)
        for(int j = 0; j < 10; j ++)
            matrix[i][j] = (i + 1) * (j + 1);


    for(int i = 0; i < 10; i ++) {
        for(int j = 0; j < 10; j ++)
            printf("%3d ", matrix[i][j]);

        printf("\n");
    }

    callback_free(matrix);
    
    return 0;
}

Запускаем с санитайзером, и всё заработало. Таким образом мы немного упростили себе заботу о ресурсах в си.

Отладочная печать

Можно посмотреть какой ресурс когда освобождается, и тоже при помощи callback‑ов. В этот раз функция не использует ничего, кроме самого указателя ptr (замыкание ни на что не замкнуто). Думаю, такие функции не редкость, поэтому имеет смысл упростить их добавление. Так в нашем коде появляется:

add_simple_callback
typedef void (*simple_fn) (void *addr);

void call_fn(void *ptr, void *resource) {
    simple_fn fn = resource;
    fn(ptr);
}

void add_simple_callback(void *ptr, simple_fn fn) {
    add_callback(ptr, (callback_t) {
	    .resource = fn,
	    .fn = call_fn
	});
}

И теперь с его помощью делаем так:

void dtor_print(void *ptr) {
    printf("Destructor on %p called\n", ptr);
}

int main() {
    int **matrix = array2d_ctor(10, 10, sizeof(int));
    add_simple_callback(matrix, dtor_print);
    puts("Constructed matrix");
    
    callback_free(matrix);
    
    return 0;
}

Запускаем и получаем:

$ gcc -std=c2x malloc_c.c
$ ./a.out
Constructed matrix
Destructor on 0x1f4a2c0 called

Код, сборка, итог

Исходный код
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#ifdef OLD

#include <stdalign.h>
#define nullptr NULL

#endif

typedef void (*simple_fn) (void *addr);
typedef void (*callback_fn)(void *addr, void *res);

typedef struct {
    void *resource;
    callback_fn fn;
} callback_t;

typedef struct {
    size_t capacity;
    size_t length;
    callback_t *callbacks;
    alignas(max_align_t) char memory[];
} chunk_t;

static inline void* chunk_to_ptr(chunk_t *chunk) {
    char *_ptr = (char *) chunk;
    return _ptr + offsetof(chunk_t, memory);
}

static inline void* ptr_to_chunk(void *ptr) {
    char *_ptr = (char *) ptr;
    return _ptr - offsetof(chunk_t, memory);
}

void* callback_alloc(size_t size) {
    chunk_t *chunk = malloc(sizeof(chunk_t) + size);
    chunk->capacity = 0;
    chunk->length = 0;
    chunk->callbacks = nullptr;

    return chunk_to_ptr(chunk);
}

void callback_free(void *ptr) {
    if(! ptr)
        return;

    chunk_t *chunk = ptr_to_chunk(ptr);
    
    for(int i = 0; i < chunk->length; i ++) {
        callback_fn fn = chunk->callbacks[i].fn;
        void *resource = chunk->callbacks[i].resource;

        fn(ptr, resource);
    }

    free(chunk->callbacks);
    free(ptr_to_chunk(ptr));
}

void add_callback(void *ptr, callback_t callback) {
    if(! ptr)
        return;

    chunk_t *chunk = ptr_to_chunk(ptr);

    if(! chunk->callbacks) {
        chunk->capacity = 10;
        chunk->length = 0;
        chunk->callbacks = malloc(10 * sizeof(callback_t));
    }

    if(chunk->length >= chunk->capacity) {
        size_t capacity = chunk->capacity;
        callback_t *old_callbacks = chunk->callbacks;
        callback_t *new_callbacks = malloc((capacity + 10) * sizeof(callback_t));
        
        memcpy(new_callbacks, old_callbacks, capacity * sizeof(callback_t));
        
        free(old_callbacks);
        
        chunk->callbacks = new_callbacks;
        chunk->capacity += 10;
    }

    chunk->callbacks[chunk->length ++] = callback;
}
void call_fn(void *ptr, void *resource) {
    simple_fn fn = resource;
    fn(ptr);
}

void add_simple_callback(void *ptr, simple_fn fn) {
    add_callback(ptr, (callback_t) {
	    .resource = fn,
	    .fn = call_fn
	});
}

void dtor_print(void *ptr) {
    printf("Destructor on %p called\n", ptr);
}

void array2d_dtor(void *ptr, void *resource) {
    void **array2d = ptr;
    size_t size_y = *(size_t *)resource;

    for(int i = 0; i < size_y; i ++)
	callback_free(array2d[i]);

    free(resource);
}

void* array2d_ctor(size_t size_y, size_t size_x, size_t elem_size) {
    void **array2d = callback_alloc(size_y * sizeof(void *));
    
    for(int i = 0; i < size_y; i ++)
        array2d[i] = callback_alloc(size_x * elem_size);

    size_t *arr_data = malloc(sizeof(size_t));
    *arr_data = size_y;
    
    add_callback(array2d, (callback_t) {
            .resource = arr_data,
            .fn = array2d_dtor
        });
    
    return array2d;
}


int main() {
    int **matrix = array2d_ctor(10, 10, sizeof(int));
    add_simple_callback(matrix, dtor_print);
    puts("Constructed matrix");
    
    callback_free(matrix);
    
    return 0;
}

Всё собиралось gcc самой последней версии (буквально собран из исходников день назад). На версиях младше 13-й или в clang компилировать с флагом -DOLD.

Весь код под WTFPL, используйте как угодно.

На этом у меня всё.

Теги:
Хабы:
Всего голосов 17: ↑16 и ↓1+20
Комментарии18

Публикации

Истории

Работа

Программист С
38 вакансий

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн