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