Проверка безопасного приведения указателя функции к другому

В своем коде я пытаюсь использовать фиктивные объекты для реализации модульности в C.

На данный момент я указываю важную функцию, полезную для каждого объекта, с помощью указателей функций, таких как деструкторы, toString, equals следующим образом:

typedef void (*destructor)(const void* obj);
typedef void (*to_string)(void* obj, int bufferSize, const char* buffer);
typedef bool (*equals)(void* obj, const void* context);

Затем в моей кодовой базе я использую указатель на функцию, совместимый с заданным typedef, для абстрактной обработки объектов, например:

struct Foo {
    int a;
} Foo;

void destroyFoo1(const Foo* p) {
   free((void*)p);
}

int main() {
    //...
    Foo* object_to_remove_from_heap = //instance of foo
    destructor d = destroyFoo1;
    //somewhere else
    d(object_to_remove_from_heap, context);
}

Код компилируется и обычно генерирует только предупреждение (первым параметром деструктора должен быть const void*, но вместо этого он const Foo*).

Однако, так как я включил -Werror, «недопустимое приведение указателя» рассматривается как ошибка. Чтобы решить эту проблему, мне нужно привести указатель функции следующим образом:

destructor d = (destructor)destroyFoo1;

Я знаю, что по стандарту const void* и const Foo* могут иметь разный размер памяти, но я предполагаю, что платформа, на которой развернут код, const void* и const Foo* размещены в одном и том же пространстве памяти и имеют одинаковый размер. В общем, я предполагаю, что приведение указателя на функцию, при котором хотя бы один аргумент указателя изменяется на какой-либо другой указатель, является безопасным приведением.

Это все хорошо, но подход показывает свою слабость, когда, например, мне нужно изменить подпись типа destructor, например, добавив новый параметр const void* context. Теперь интересное предупреждение отключено, а количество параметров в вызове указателя функции не совпадает:

//now destructor is
typedef void (*destructor)(const void* obj, const void* context);

void destroyFoo1(const Foo* p) {
   free((void*)p);
}

destructor d = (destructor)destroyFoo1; //SILCENCED ERROR!!destroyFoo1 has invalid parameters number!!!!
//somewhere else
d(object_to_remove_from_heap, context); //may mess the stack

Мой вопрос: есть ли способ проверить, действительно ли указатель функции можно безопасно преобразовать в другой (и сгенерировать ошибку компиляции, если нет)?, что-то вроде:

destructor d = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo1);

Что-то, что если мы проходим destroyFoo1, то все нормально, но если мы проходим destroyFoo2, компилятор жалуется.

Ниже код, который обобщает проблему

typedef void (*destructor)(const void* obj, const void* context);

typedef struct Foo {
    int a;
} Foo;

void destroyFoo1(const Foo* p, const void* context) {
   free((void*)p);
   if (*((int*)context) == 0) {
       printf("hello world\n");
   }
}

void destroyFoo2(const Foo* p) {
    free((void*)p);
}

int main() {
    //this is(in my case) safe
    destructor destructor = (destructor) destroyFoo1;
    //this is really a severe error!
    //destructor destructor = (destructor) destroyFoo2;

    Foo* a = (Foo*) malloc(sizeof(Foo));
    a->a = 3;
    int context = 5;
    if (a != NULL) {
        //call a destructor: if destructor is destroyFoo2 this is a SEVERE ERROR!
        //calling a function accepting a single parameter with 2 parameters!
        destructor(a, &context);
    }
}

Спасибо за любой ответ

Возможный дубликат Указатель функции приведен к другой подписи

Nellie Danielyan 09.04.2019 17:10

Попробуйте -Wbad-function-cast вариант

Nellie Danielyan 09.04.2019 17:23

@NellieDanielyan Я посмотрел на вопрос, но не понимаю, как это решит проблему. Насколько я понял, цель макроса CALL_MAYBE состоит в том, чтобы автоматически проверять нуль указателя на функцию, а не обрабатывать правильное/неправильное количество параметров.

Koldar 09.04.2019 17:42

@NellieDanielyan что касается -Wbad-function-cast: я попробовал MWE, который я предоставил, и он не выдает ошибку (выдается предупреждение -Wincompatible-pointer-types)

Koldar 09.04.2019 17:45

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

Nellie Danielyan 09.04.2019 17:51

Не будет ли это действительно небезопасно? Очень возможно, что мне все еще нужно предупреждение incompatible-pointer-types в том же файле для совершенно не связанного кода (например, float* b = 5; int* a = b).

Koldar 09.04.2019 18:47

вы все еще можете использовать его как предупреждение вместо ошибки, оставив -Werror включенным для остальных. #pragma GCC diagnostic warning "Wincompatible-pointer-types"

Nellie Danielyan 09.04.2019 19:01
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
149
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Прошло некоторое время, но не должен ли код для назначения указателя функции быть:

//this is okay
destructor destructor1 = &destructorFoo1;

//this should throw a compilation error!
destructor destructor2 = &destructorFoo2;

Обновлено:

Ладно, я ушел и посмотрел на это поближе.

Если я изменю объявление указателя функции на использование const Foo* p, а не const void* obj, чтобы мы не полагались на приведение, чтобы скрыть несовместимость между void* и Foo*, я получаю предупреждение с настройками компилятора по умолчанию.

Затем, приведя destroyFoo2 к (деструктору), вы затем скроете это предупреждение, заставив компилятор рассматривать функцию как этот тип.

Я предполагаю, что это подчеркивает подводные камни кастинга.

Я проверил это, используя следующий код:

typedef struct Foo
{
    int a;
} Foo;

typedef void (*destructor)(const Foo* p, const void* context);


void destroyFoo1(const Foo* p, const void* context);
void destroyFoo1(const Foo* p, const void* context) 
{
   free((void*)p);
   if (*((int*)context) == 0) {
       printf("hello world\n");
   }
}
void destroyFoo2(const Foo* p);
void destroyFoo2(const Foo* p) 
{
    free((void*)p);
}
int main(void)
{
    //this is okay
    destructor destructor1 =  destroyFoo1;
    //this triggers a warning
    destructor destructor2 = destroyFoo2;
    //This doesn't generate a warning
    destructor destructor3 = (destructor)destroyFoo2;

}

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

Koldar 09.04.2019 16:58

в C имя функции «распадается» до указателя на эту функцию. Следовательно, его можно присвоить переменной правильного типа функции без необходимости использования оператора адреса.

David Jones 09.04.2019 17:02

После РЕДАКТИРОВКИ: я думаю, вы неправильно поняли вопрос (извините за неясность): проблема не в удалении предупреждения (само приведение может решить проблему), а в том, чтобы определить, безопасно ли приведение или оно приведет к тяжелый УБ. с вашим destructor3 вы не решаете проблему, потому что после назначения destructor3 программа перейдет к destructor3(a, &context);: там мы попытаемся вызвать destructor3 по 2 параметрам, в то время как destroyFoo2 имеет только 2 из них (жесткий UB происходит без компилятора, изменяющего вас!) .

Koldar 09.04.2019 18:41

Изменить тип первого параметра destructor также нельзя: хотя Foo — это всего лишь пример, в кодовой базе есть несколько разных структур (Bar, Baz и так далее). У каждого из них есть свой деструктор. Следовательно, единственным возможным типом первого параметра destructor является void*

Koldar 09.04.2019 18:42

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

ChrisBD 10.04.2019 09:08
Ответ принят как подходящий

Хорошо, я думаю, что понял это, но это не так просто.

Прежде всего, проблема заключается в том, что CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS необходимо сравнивать во время компиляции 2 подписи: входную (данную из указателя входной функции, например, destroyFoo1) и базовую (то есть подпись типа destructor): если мы реализуем метод это делает это, мы можем проверить, являются ли 2 подписи «совместимыми» или нет.

Мы делаем это, используя препроцессор C. Основная идея заключается в том, что для каждой функции, которую мы хотели бы использовать в качестве destructor, определен макрос. CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS также будет макросом, который просто генерирует имя макроса на основе сигнатуры типа destructor: если имя макроса, сгенерированное в CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS, существует, мы предполагаем, что указатель функции совместим с destructor, и мы приводим к нему. В противном случае мы выдаем ошибку компиляции. Поскольку нам нужно определение макроса для каждой функции, которую мы хотим использовать в качестве деструктора, это может быть дорогостоящим решением в огромных кодовых базах.

Примечание. Реализация зависит от GCC (в ней используются варианты ## и _Pragma, но я думаю, что ее можно легко портировать и на некоторые другие компиляторы).

Так, например:

#define FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 1
void destroyFoo1(const Foo* p, const void* context);

Значение макроса — это просто постоянное число. Что важно, так это имя макроса, которое имеет четко определенную структуру. Соглашение, которое вы используете, не имеет значения, просто выберите и придерживайтесь одного из них. Здесь я использовал следующее соглашение:

//macro (1)
"FUNCTION_POINTER_" typdefName "_" returnType "_" functionName "_" typeparam1 "_" typeparam2 ...

Теперь мы собираемся определить макрос, который проверяет, совпадают ли две подписи. Чтобы помочь нам, мы используем проект Р99. Мы будем использовать несколько макросов из проекта, поэтому вы можете реализовать такие макросы самостоятельно, если не хотите на них полагаться:

#define CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(functionName) \
    _ENSURE_FUNCTION_POINTER(1, destructor, void, functionName, voidConstPtr, voidConstPtr)

#define _ENSURE_FUNCTION_POINTER(valueToCheck, castTo, expectedReturnValue, functionName, ...) \
        P99_IF_EQ(valueToCheck, _GET_FUNCTION_POINTER_MACRO(castTo, expectedReturnValue, functionName, ## __VA_ARGS__)) \
            ((castTo)(functionName)) \
            (COMPILE_ERROR())

#define COMPILE_ERROR() _Pragma("GCC error \"function pointer casting error!\"")

Входными данными макроса является значение макроса (1) для проверки (т. е. 1 в данном случае значение из макроса функции), typedef, по которому мы хотим проверить (castTo), тип возвращаемого значения, который мы ожидаем, что functionName будет иметь и functionName, переданный пользователем CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS (например, destroyFoo1 или destroyFoo2). Variadic — это типы каждого параметра. Эти параметры должны быть такими же, как в (1): мы пишем voidConstPtr, потому что мы не можем использовать const void* в имени макроса.

_GET_FUNCTION_POINTER_MACRO генерирует макрос, связанный с подписью, которую мы ожидаем от functionName:

#define _DEFINE_FUNCTION_POINTER_OP(CONTEXT, INDEX, CURRENT, NEXT) P99_PASTE(CURRENT, NEXT)
#define _DEFINE_FUNCTION_POINTER_FUNC(CONTEXT, CURRENT, INDEX) P99_PASTE(_, CURRENT)

#define _GET_FUNCTION_POINTER_MACRO(functionPointerType, returnValue, functionName, ...) \
    P99_PASTE(FUNCTION_POINTER, _, functionPointerType, _, returnValue, _, functionName, P99_FOR(, P99_NARG(__VA_ARGS__), _DEFINE_FUNCTION_POINTER_OP, _DEFINE_FUNCTION_POINTER_FUNC, ## __VA_ARGS__))

//example
_GET_FUNCTION_POINTER_MACRO(destructor, void, destroyFoo2, voidConstPtr, voidConstPtr)
//it generates
FUNCTION_POINTER_destructor_void_destroyFoo2_voidConstPtr_voidConstPtr

Так, например:

#define FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 1
void destroyFoo1(const Foo* p, const void* context) 
{
   free((void*)p);
   if (*((int*)context) == 0) {
       printf("hello world\n");
   }
}

void destroyFoo2(const Foo* p) 
{
    free((void*)p);
}
int main(void)
{
    //this will work:
    //FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 
    //macro exist and is equal to 1
    destructor destructor1 =  CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo1);

    //this raise a compile error:
    //FUNCTION_POINTER_destructor_void_destroyFoo2_voidConstPtr_voidConstPtr
    //does not exist (or exists but its value is not 1)
    destructor destructor2 = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo2);
}

Важные заметки

на самом деле voidConstPtr или даже void в имени макроса — это просто строки. Все бы работало, даже если бы вы заменили void на helloWorld. Они просто следуют условности.

Последнее, что нужно понять, это условие, реализованное P99_IF_EQ в _ENSURE_FUNCTION_POINTER: если вывод _GET_FUNCTION_POINTER_MACRO является существующим макросом, препроцессор автоматически заменит его своим значением, в противном случае имя макроса останется прежним; если макрос заменен на 1 (сгенерированный макрос _GET_FUNCTION_POINTER_MACRO существует и равен 1), мы будем считать, что это достигается только потому, что макрос, определенный разработчиком (1), и мы предполагаем, что functionName соответствует destructor. В противном случае мы выдадим ошибку времени компиляции.

Другие вопросы по теме