Выделение указателя функции на другой тип

Скажем, у меня есть функция, которая принимает указатель функции void (*)(void*) для использования в качестве обратного вызова:

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

Теперь, если у меня есть такая функция:

void my_callback_function(struct my_struct* arg);

Могу ли я сделать это безопасно?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

Я рассмотрел этот вопрос, и я просмотрел некоторые стандарты C, которые говорят, что вы можете использовать для "совместимых указателей функций", но я не могу найти определение того, что означает "совместимый указатель функции".

Ответ 1

Что касается стандарта Си, если вы приведете указатель функции к указателю на функцию другого типа, а затем вызовете его, это будет неопределенное поведение. См. Приложение J.2 (информативное):

Поведение не определено в следующих обстоятельствах:

  • Указатель используется для вызова функции, тип которой не совместим с указанным типом (6.3.2.3).

Раздел 6.3.2.3, пункт 8 гласит:

Указатель на функцию одного типа может быть преобразован в указатель на функцию другого типа и обратно; результат должен сравниваться равным исходному указателю. Если преобразованный указатель используется для вызова функции, тип которой не совместим с указанным типом, поведение не определено.

Другими словами, вы можете привести указатель функции к другому типу указателя на функцию, вернуть его снова и вызвать, и все будет работать.

Определение совместимости несколько сложнее. Это можно найти в разделе 6.7.5.3, параграф 15:

Для совместимости двух типов функций оба должны указывать совместимые типы возврата 127.

Кроме того, списки типов параметров, если они присутствуют, должны согласовываться по количеству параметров и при использовании терминатора многоточия; соответствующие параметры должны иметь совместимые типы. Если один тип имеет список типов параметров, а другой тип указывается декларатором функции, который не является частью определения функции и содержит пустой список идентификаторов, список параметров не должен иметь терминатора с многоточием и тип каждого параметра должен быть совместимым с типом, который является результатом применения продвижения аргумента по умолчанию. Если один тип имеет список типов параметров, а другой тип определен в определении функции, которое содержит (возможно, пустой) список идентификаторов, оба должны совпадать по количеству параметров, и тип каждого параметра прототипа должен быть совместим с типом это является результатом применения продвижения аргумента по умолчанию к типу соответствующего идентификатора. (При определении совместимости типов и составного типа каждый параметр, объявленный с типом функции или массива, принимается как имеющий скорректированный тип, а каждый параметр, объявленный с квалифицированным типом, принимается как имеющий неквалифицированную версию своего объявленного типа.)

127) Если оба типа функций являются "старыми", типы параметров не сравниваются.

Правила определения совместимости двух типов описаны в разделе 6.2.7, и я не буду их здесь цитировать, так как они довольно длинные, но вы можете прочитать их в проекте стандарта C99 (PDF).

Соответствующее правило здесь, в разделе 6.7.5.1, параграф 2:

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

Следовательно, поскольку void* не совместим со struct my_struct*, указатель функции типа void (*)(void*) не совместим с указателем функции типа void (*)(struct my_struct*), поэтому приведение из функциональных указателей является технически неопределенным поведением.

На практике, однако, в некоторых случаях вы можете спокойно пользоваться указателями функций приведения. В соглашении о вызовах x86 аргументы помещаются в стек, и все указатели имеют одинаковый размер (4 байта в x86 или 8 байтов в x86_64). Вызов указателя на функцию сводится к тому, чтобы передать аргументы в стек и сделать косвенный переход к цели указателя на функцию, и, очевидно, нет понятия типов на уровне машинного кода.

То, что вы определенно не можете сделать:

  • Приведение между указателями на функции различных соглашений о вызовах. Вы испортите стек и, в лучшем случае, потерпите крах, в худшем случае преуспеете в молчании с огромной дырой в безопасности. В программировании Windows вы часто передаете функциональные указатели. Win32 ожидает, что все функции обратного вызова будут использовать stdcall вызовах stdcall (к которому CALLBACK макросы CALLBACK, PASCAL и WINAPI). Если вы передадите указатель на функцию, которая использует стандартное соглашение о вызовах C (cdecl), это приведет к плохому результату.
  • В C++ приведена между указателями на функции-члены класса и обычными указателями на функции. Это часто сбивает с толку C++ новичков. Функции-члены класса имеют скрытый параметр this, и если вы преобразуете функцию-член в обычную функцию, this объект не будет использоваться, и, опять же, это приведет к серьезным последствиям.

Еще одна плохая идея, которая иногда может работать, но это также неопределенное поведение:

  • Приведение между указателями на функции и обычными указателями (например, приведение void (*)(void) к void*). Указатели на функции не обязательно имеют тот же размер, что и обычные указатели, поскольку на некоторых архитектурах они могут содержать дополнительную контекстную информацию. Это, вероятно, будет хорошо работать на x86, но помните, что это неопределенное поведение.

Ответ 2

Недавно я спросил об этой же проблеме в отношении некоторого кода в GLib. (GLib - это базовая библиотека для проекта GNOME и написанная на C.) Мне сказали, что от этого зависит весь дизайн слотов.

Всюду по коду существует множество экземпляров кастинга от типа (1) - (2):

  • typedef int (*CompareFunc) (const void *a, const void *b)
  • typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

Это обычная цепочка с такими вызовами:

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

Смотрите здесь, в g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Ответы выше подробные и, вероятно, правильные - , если вы сидите в комитете по стандартам. Адам и Йоханнес заслуживают признания за свои хорошо исследованные ответы. Однако, в дикой природе, вы обнаружите, что этот код работает нормально. Спорные? Да. Рассмотрим это: GLib компилирует/работает/тестирует на большом количестве платформ (Linux/Solaris/Windows/OS X) с широким спектром компиляторов/компоновщиков/загрузчиков ядра (GCC/CLang/MSVC). Конечно, стандарты будут прокляты.

Я потратил некоторое время на размышления об этих ответах. Вот мой вывод:

  • Если вы пишете библиотеку обратного вызова, это может быть ОК. Caveat emptor - используйте на свой страх и риск.
  • Иначе, не делай этого.

Думая глубже после написания этого ответа, я не удивлюсь, если код для компиляторов C использует этот же трюк. И поскольку (большинство/все?) Современные компиляторы C загружаются, это подразумевает, что трюк безопасен.

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

Ответ 3

В действительности, дело не в том, можете ли вы. Тривиальное решение

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

Хороший компилятор будет генерировать код для my_callback_helper, если это действительно необходимо, и в этом случае вы были бы рады, если бы он это сделал.

Ответ 4

У вас есть совместимый тип функции, если тип возвращаемого типа и типы параметров совместимы - в основном (это сложнее в реальности:)). Совместимость - это то же самое, что и "тот же самый тип", который более лёгкий, чтобы иметь разные типы, но все еще имеет определенную форму: "Эти типы почти одинаковы". Например, в C89 две структуры были совместимы, если они были идентичны друг другу, но только их имя было другим. C99, похоже, изменил это. Цитата из c обоснованным документом (очень рекомендуемое чтение, кстати!):

Объявления типа структуры, объединения или перечисления в двух разных единицах перевода формально не объявляют один и тот же тип, даже если текст этих объявлений поступает из одного и того же файла включения, поскольку сами единицы перевода не пересекаются. Таким образом, Стандарт определяет дополнительные правила совместимости для таких типов, так что если два таких объявления достаточно схожи, они совместимы.

Тем не менее, строгое поведение undefined, потому что ваша функция do_stuff или кто-то другой вызовет вашу функцию с указателем функции, имеющим void* в качестве параметра, но ваша функция имеет несовместимый параметр. Но, тем не менее, я ожидаю, что компиляторы будут компилировать и запускать его без стонов. Но вы можете сделать чище, если у вас есть другая функция, которая принимает void* (и регистрирует ее как функцию обратного вызова), которая тогда просто вызовет вашу фактическую функцию.

Ответ 5

Так как код C компилируется в инструкцию, которая не заботится о типах указателей, достаточно использовать код, который вы упомянули. У вас возникнут проблемы, когда вы запустите do_stuff с функцией обратного вызова и указателем на что-то еще, а затем структуру my_struct в качестве аргумента.

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

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

или...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

В принципе, вы можете накладывать указатели на все, что вам нравится, если данные продолжают иметь смысл во время выполнения.

Ответ 6

Указатели Void совместимы с другими типами указателей. Это основа того, как работают функции malloc и mem (memcpy, memcmp). Как правило, в C (а не С++) NULL - это макрос, определенный как ((void *)0).

Взгляните на пункт 6.3.2.3 (пункт 1) на C99:

Указатель на void может быть преобразован в указатель или из указателя на любой неполный или тип объекта

Ответ 7

Если вы думаете о том, как работают вызовы функций на C/С++, они нажимают определенные элементы в стеке, переходят в новое местоположение кода, выполняются, а затем возвращают стек. Если указатели на функции описывают функции с одним и тем же типом возвращаемого значения и одинаковым числом аргументов, вы должны быть в порядке.

Таким образом, я думаю, вы должны быть в состоянии сделать это безопасно.

Ответ 8

struct * → void * и наоборот, на мой взгляд, можно безопасно преобразовать.

Самая большая проблема, на мой взгляд, заключается в различном количестве и размере аргументов в __stdcall (ведь это самый, если не только используемый тип в обратных вызовах). Функция получает переменные, нажатые в стек вызывающим, затем функция выталкивает их из стека. Если размер отличается от приложения, произойдет сбой. Только возвращаемое значение не так важно, потому что обычно возвращается через EAX/RAX, поэтому вы можете даже передать его BYTE.

Лучше всего __fastcall (и __stdcall на x68-64), потому что значения передаются с помощью регистров, а не стека, или __cdecl, потому что caller (not function) очищает стек. По-моему, это может быть самым безопасным для конвертирования. Может быть замечен в методе обратного вызова atexit(). Если указатель на действительную функцию (void) будет вызываться, указав указатель как функцию (с большим количеством аргументов), это на 100% безопасно. Caller нажимает на стек, и вызывающий абонент выводит стек. Аргументы игнорируются. Но это не будет работать неопознанно.

Вероятно, из-за этого int __cdecl main(); могут быть объявлены как с argc, так и без argv?