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

от автора

В данной статье будет описано создание кастомного аллокатора на си 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, используйте как угодно.

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


ссылка на оригинал статьи https://habr.com/ru/articles/721188/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *