Проверка, безопасно ли привести указатель функции в другой - PullRequest
0 голосов
/ 09 апреля 2019

В своем коде я пытаюсь использовать фиктивные объекты для выполнения модульности в 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);
    }
}

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

Ответы [ 2 ]

0 голосов
/ 10 апреля 2019

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

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

Мы делаем это, используя препроцессор Си. Основная идея состоит в том, что каждая функция, которую мы хотели бы использовать как destructor, имеет определенный макрос. CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS также будет макросом, который просто генерирует имя макроса на основе сигнатуры типа destructor: если имя макроса, сгенерированное в CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS, существует, то мы предполагаем, что functionPointer совместим с 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 ...

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

#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. В противном случае мы выдадим ошибку времени компиляции.

0 голосов
/ 09 апреля 2019

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

//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;

}
...