Добавляем деструкторы в 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, используйте как угодно.
На этом у меня всё.