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

Комментарии 18

ЗакрепленныеЗакреплённые комментарии

Метод, в определённом смысле, широко известный, т.к. массово применяется в микрософтовских API. В частности для строк и где-то ещё.

Если программа взаимодействует с сторонними библиотеками и обменивается с ними объектами, может быть нежданным сюрпризом когда в свой free() приедет чужой указатель, выделенный на стороне (и без такого трюка).

Поэтому, возможно, нет смысла в таком трюке вообще, и нужно просто определить свои стуктуры, где эти деструкторы хранить в явном виде. Хотя да, иногда бывается хочется в неявном...

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

Можно всё вывернуть немного наоборот и получится проще, без опасного "void*", который обычно является источником багов:

typedef struct callback_functor {
    void (*callback_fn)(struct callback_fuctor *, void *addr);
    /* This structure may be extended... */
} callback_functor_t;

Идея в том, что при необходимости может быть создана другая структура, первым элементом которой будет callback_functor_t (таким образом реализуется "наследование" на C). Затем функция free может вызвать callback_functor_t->callback_fn() с передачей ему указателя на сам callback_functor_t (хранящийся "перед началом" выделенного блока), а внутри коллбэка тип callback_functor_t можно "привести" к типу расширенной структуры с помощью широко известного макроса container_of. Здесь во всей цепочке отсутствует void* (кроме самого указателя на освобождаемую память) и нет практически возможности совместить что-то несовместимое, что привело бы к ошибке.

И это скорей не аналог замыкания, а аналог std::function (фунцкионального объекта), но в сишном стиле.

То, что у вас коллбэки навешиваются после аллокации -- скорей плохо. Это же просто можно забыть сделать. Будет лучше, если бы функция аллокации сразу принимала указатель на коллбэк и его размер, и копировала бы его по значению (в адрес ниже возвращаемого указателя, разумеется выравненный на max_align_t).

Чтоб размер не принимать явно, функция замещающая malloc может быть сделана макросом (внутри которого будет делаться sizeof(*(callback_argument)). Разумеется здесь возникает проблема, что поскольку предполагается расширение типа callback_functor_t другим типом, то собственно типы получаются разные и функция замещающая malloc не может проверить, что ей не подсунули что-то непотребное (либо тип будет приведен к callback_functor_t, но тогда потеряется тип и размер расширенного функтора). Это можно обойти, если договориться, что все взаимозаменяемые типы (унаследованные и базовый) будут содержать специальный именованный член -- пустую структуру, например, первым элементом структуры. Тогда можно передавать ссылку на этот член и легко преобразовывать указатель между разными типами с помощью опять же макроса container_of. Это опять же лучше, чем void*, т.к. совсем уж что угодно присвоить к чему угодно компилятор не позволит, и проверка типов продолжит работать (хотя для программиста появляется возможность сделать ошибку в определении структур, впрочем там сложности никакой и ошибиться тяжело). В данном случае в макросе можно адресовать специальный именованный член стурктуры, чтоб убедиться что стрктура имеет нужный тип (либо там нет такого члена). Это не совсем утиная типизация, т.к. этот самый спец. член всё же может иметь разные типы (пустой структуры, но разные, т.к. ключевое слово struct каждый раз декларирует новый, не совместимый тип) для каждых несовместимых классов объектов. Не знаю понятно ли объяснил, как-то так:

typedef struct {} callback_func_tag;

typedef struct callback_functor {
    callback_func_tag tag;
    void (*destructor)(struct callback_fuctor *, void *addr);
    /* This structure may be extended... */
} callback_functor_t;


#define special_malloc(size, deleter) \
    _special_malloc(size, &deleter->tag, sizeof(*(deleter)))

void* _special_malloc(size_t size, callback_func_tag *tag, size_t deleter_size)
{
    size_t bottom_size = (deleter_size + sizeof(size_t) - 1) & (sieof(size_t) - 1);
    bottom_size = (bottom_size + _Alignof(max_align_t) - 1) & (_Alignof(max_align_t) - 1);
    void *ptr = malloc(size + bottom_size);
    if (!ptr) return NULL;

    void *result = (char*)ptr + bottom_size;
    ((size_t*)result)[-1] = bottom_size;
    memcpy(ptr, tag, deleter_size)

    return result;
}

void special_free(void *ptr)
{
    if (!ptr) return;
  
    size_t bottom_size = ((size_t*)ptr)[-1];
    callback_functor_t *functor = (char*)ptr - bottom_size;
    functor->destructor(functior, ptr);
    free(ptr);
}


struct extended_callbck {
    callback_func_tag tag;
    callback_functor_t base;
    // context follows:
    char *zzz;
    int xxx; 
};

void example_destructor(callback_functor_t *pthis, void *addr)
{
    struct extended_callback *cb = container_of(pthis, callback_func_tag, tag);
    cb->zzz, cb->xxx...;
}

char* example1(size_t n)
{
    char *mem = special_malloc(n, &(struct extended_callback){
                    .base.destructor = example_destructor,
                    .zzz = NULL, .xxx = 2
                  });
    }
    return mem;  
}

void example2(char *p)
{
    special_free(p);  
}

Ну разумеется callback_functor_t и всё от него наследуемое -- должно быть перемещаемым (на него нигде в памяти не должно сохраняться указателей).

В принципе то же что у автора примерно, только немного другие механизмы, где нет void*. Реализация container_of макроса может выглядеть вот так:

#define container_of(ptr, type, member) ((type*)((char*)(1 ? (ptr) : &((type*)0)->member) - offsetof(type, member)))

В линуксе по смыслу такая же, но полагается на GNU-расширения. Впрочем, пустые структуры тоже не строгое соответствие ISO-C.

PS: но может проще писать на C++ тогда... Там std::unique_ptr уже сразу из коробки.

Добавляем деструкторы в С:
clang -> clang++

Т.к. у Вас добавление колбэков сделано отдельно, то (возможно) лучше его закрыть неким подобием локов (системные, или свои) при работе с многопоточностью. (Или прямо напишите, что эти функции однопоточные).

Если не хочется разбираться с локами, то лучше сделать выделение памяти и добавление колбэка (например, одного) в одном вызове функции.

С обработкой ошибок что-то сделать: если добавляем колбэк и malloc вернёт NULL, то всё крэшнется.

Примечание: если вооружиться опытом C++, то:

  • достаточно одного колбэка (добавление можно затолкать прямо в вызов alloc);

  • дополнительные данные (resource) не требуются, т.к. есть доступ к удаляемому объекту и если сильно хочется, то можно добавлять дополнительные данные туда.

Про локи и обработку ошибок справедливо, но не хотелось сильно загромождать код. Как-никак это больше "смотрите как могу", чем готовый продукт)

Можно сделать как в плюсах, но я бы для этого воспользовался иной конструкцией. Механизм callback-ов, как мне кажется, должен позволять навесить их несколько штук, а resource может использоваться для проделывания всяких штук за пределами объекта, например, уведомить какой-то кусок кода, что данного объекта больше не существует или послать что-то в сокет.

В общем, если делать только деструкторы, то разумно сделать так, но я больше думаю о callback-ах как о подписке на событие удаления объекта

А ничего, что alignas specifier (since C++11)? Это уже не чистый С, а какой-то кадавр получается

Начиная с C11 есть ключевое слово _Alignas и в хедере stdalign.h определён макрос alignas, который в него раскрывается. Но тут я тут пользуюсь самым новым стандартом C23 (он же c2x), в котором alignas сделали ключевым словом (и добавили nullptr), а для старых версий просто добавляете флаг -DOLD, который превращает это в валидный C11 код. Попробуйте скомпилировать, на gcc-10 и clang-11 точно работает

Основная польза от деструкторов в том, что их сложно забыть вызвать. Потому и уходим от сырых указателей к умным

Можно обойтись одним колбеком на чанк, как наиболее распространенный случай. А если нужно больше, то уложить их в сам освобождаемый объект и вызвать из первого. Меньше аллокаций - меньше проблем.

Здравствуйте!

Основная задача деструкторов в C++ - вызывать код при разрушении автоматических переменных. Ваше решение будто-бы никак этот вопрос не решает.

А как похитрее завернуть malloc/free, чтобы еще хитрее выстрелить себе в ногу в случае чего - то, да с этим ваш код справляется.

В голом C нет неявного потока управления, и это мотивация отсутствия там деструкторов. Жалко что не ввели defer в C23...

автоматические - это как правило переменные, размещенные на стеке. Их разрушение происходит без участия программиста по авершению контекста. Тут можно вести речь про автоматичекий указатель, которому выделена память в куче, и типичную ошибку начинающего сиониста - забывчивость освобождения кучи по завершению контекста.

Кмк, данный подход можно дополнить небольшим #define return() который будет вызывать этот callback перед возвратом. Если делать универсальный макрос, то там придется попотеть, "настраивая" его под конкретный вызов с возвращаемым параметром. Но .. тоже решаемо как мне видится. Заодно, такой макрос может решать вопрос отложенного вызова типа defer() языка Go..

Кмк, автору стоит доработать пакетик. :)

В GCC/Clang есть __attribute__((cleanup(function))) var. Но оно не решает проблему полностью, т.к. дальше захочется подсчёта ссылок. А с этим сразу сложно, т.к. в GCC вложенные функции с (неработающими -- т.к. исполняемый стек) трамплинами, в Clang есть blocks (недолямбды...), и всё это нестандарт. Потом захочется отличать перемещение и копирование. И так будет заново изобретаться C++...

попробуйте talloc (hierarchical, reference counted memory pool system with destructors)

Метод, в определённом смысле, широко известный, т.к. массово применяется в микрософтовских API. В частности для строк и где-то ещё.

Если программа взаимодействует с сторонними библиотеками и обменивается с ними объектами, может быть нежданным сюрпризом когда в свой free() приедет чужой указатель, выделенный на стороне (и без такого трюка).

Поэтому, возможно, нет смысла в таком трюке вообще, и нужно просто определить свои стуктуры, где эти деструкторы хранить в явном виде. Хотя да, иногда бывается хочется в неявном...

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

Можно всё вывернуть немного наоборот и получится проще, без опасного "void*", который обычно является источником багов:

typedef struct callback_functor {
    void (*callback_fn)(struct callback_fuctor *, void *addr);
    /* This structure may be extended... */
} callback_functor_t;

Идея в том, что при необходимости может быть создана другая структура, первым элементом которой будет callback_functor_t (таким образом реализуется "наследование" на C). Затем функция free может вызвать callback_functor_t->callback_fn() с передачей ему указателя на сам callback_functor_t (хранящийся "перед началом" выделенного блока), а внутри коллбэка тип callback_functor_t можно "привести" к типу расширенной структуры с помощью широко известного макроса container_of. Здесь во всей цепочке отсутствует void* (кроме самого указателя на освобождаемую память) и нет практически возможности совместить что-то несовместимое, что привело бы к ошибке.

И это скорей не аналог замыкания, а аналог std::function (фунцкионального объекта), но в сишном стиле.

То, что у вас коллбэки навешиваются после аллокации -- скорей плохо. Это же просто можно забыть сделать. Будет лучше, если бы функция аллокации сразу принимала указатель на коллбэк и его размер, и копировала бы его по значению (в адрес ниже возвращаемого указателя, разумеется выравненный на max_align_t).

Чтоб размер не принимать явно, функция замещающая malloc может быть сделана макросом (внутри которого будет делаться sizeof(*(callback_argument)). Разумеется здесь возникает проблема, что поскольку предполагается расширение типа callback_functor_t другим типом, то собственно типы получаются разные и функция замещающая malloc не может проверить, что ей не подсунули что-то непотребное (либо тип будет приведен к callback_functor_t, но тогда потеряется тип и размер расширенного функтора). Это можно обойти, если договориться, что все взаимозаменяемые типы (унаследованные и базовый) будут содержать специальный именованный член -- пустую структуру, например, первым элементом структуры. Тогда можно передавать ссылку на этот член и легко преобразовывать указатель между разными типами с помощью опять же макроса container_of. Это опять же лучше, чем void*, т.к. совсем уж что угодно присвоить к чему угодно компилятор не позволит, и проверка типов продолжит работать (хотя для программиста появляется возможность сделать ошибку в определении структур, впрочем там сложности никакой и ошибиться тяжело). В данном случае в макросе можно адресовать специальный именованный член стурктуры, чтоб убедиться что стрктура имеет нужный тип (либо там нет такого члена). Это не совсем утиная типизация, т.к. этот самый спец. член всё же может иметь разные типы (пустой структуры, но разные, т.к. ключевое слово struct каждый раз декларирует новый, не совместимый тип) для каждых несовместимых классов объектов. Не знаю понятно ли объяснил, как-то так:

typedef struct {} callback_func_tag;

typedef struct callback_functor {
    callback_func_tag tag;
    void (*destructor)(struct callback_fuctor *, void *addr);
    /* This structure may be extended... */
} callback_functor_t;


#define special_malloc(size, deleter) \
    _special_malloc(size, &deleter->tag, sizeof(*(deleter)))

void* _special_malloc(size_t size, callback_func_tag *tag, size_t deleter_size)
{
    size_t bottom_size = (deleter_size + sizeof(size_t) - 1) & (sieof(size_t) - 1);
    bottom_size = (bottom_size + _Alignof(max_align_t) - 1) & (_Alignof(max_align_t) - 1);
    void *ptr = malloc(size + bottom_size);
    if (!ptr) return NULL;

    void *result = (char*)ptr + bottom_size;
    ((size_t*)result)[-1] = bottom_size;
    memcpy(ptr, tag, deleter_size)

    return result;
}

void special_free(void *ptr)
{
    if (!ptr) return;
  
    size_t bottom_size = ((size_t*)ptr)[-1];
    callback_functor_t *functor = (char*)ptr - bottom_size;
    functor->destructor(functior, ptr);
    free(ptr);
}


struct extended_callbck {
    callback_func_tag tag;
    callback_functor_t base;
    // context follows:
    char *zzz;
    int xxx; 
};

void example_destructor(callback_functor_t *pthis, void *addr)
{
    struct extended_callback *cb = container_of(pthis, callback_func_tag, tag);
    cb->zzz, cb->xxx...;
}

char* example1(size_t n)
{
    char *mem = special_malloc(n, &(struct extended_callback){
                    .base.destructor = example_destructor,
                    .zzz = NULL, .xxx = 2
                  });
    }
    return mem;  
}

void example2(char *p)
{
    special_free(p);  
}

Ну разумеется callback_functor_t и всё от него наследуемое -- должно быть перемещаемым (на него нигде в памяти не должно сохраняться указателей).

В принципе то же что у автора примерно, только немного другие механизмы, где нет void*. Реализация container_of макроса может выглядеть вот так:

#define container_of(ptr, type, member) ((type*)((char*)(1 ? (ptr) : &((type*)0)->member) - offsetof(type, member)))

В линуксе по смыслу такая же, но полагается на GNU-расширения. Впрочем, пустые структуры тоже не строгое соответствие ISO-C.

PS: но может проще писать на C++ тогда... Там std::unique_ptr уже сразу из коробки.

Идея с расширяемой структурой очень годная. Такой подход действительно сильно мощнее/удобнее того, что сделал я, но реализацию, кажется, можно сделать немного проще.

По стандарту можно кастовать указатель на структуру и на её первый элемент. Я думаю, наследники обязаны иметь функциональность базового класса, и тогда tag не нужен. Просто во всех наследников положим первым полем callback_base типа callback_functor_t. Отпадает необходимость и в макросе, так как теперь перегонять в начальный тип можно напрямую

Я в отладке просто переназначаю malloc и free, или new delete на свои вызовы где создаются, таблицы и отслеживается все распределение памяти. А по завершению вызываю функцию которая проверяет-закрывает все распределенное и выводит список проблем.

В релизе же все это отключено.

или new delete

Так если вам доступны плюсы, то зачем тогда праиться с сишными проблемами

Потому что на си я поступаю точно так же. Переназначаю аллокатор дефайнами.

Можно и еще суровее, через патч самого вызова. Но это уже хакерство в чистом виде и нужно довольно редко.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории