Передача покажчика функції на інший тип


88

Скажімо, у мене є функція, яка приймає 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
Я дещо новачок, але що означає " покажчик функції void ( ) (void )". Це вказівник на функцію, яка приймає void * як аргумент і повертає void
Digital Gal

2
@Myke: void (*func)(void *)означає, що funcце вказівник на функцію з підписом типу, наприклад void foo(void *arg). Тож так, ти маєш рацію.
mk12

Відповіді:


121

Що стосується стандарту C, якщо ви приводите покажчик функції до покажчика функції іншого типу, а потім викликаєте це, це невизначена поведінка . Див. Додаток 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:

Щоб два типи покажчиків були сумісними, обидва повинні бути однаково кваліфікованими, і обидва повинні бути вказівниками на сумісні типи.

Отже, оскільки a void* не є сумісним з a struct my_struct*, покажчик функції типу void (*)(void*)не сумісний з покажчиком функції типу void (*)(struct my_struct*), тож це лиття покажчиків функцій є технічно невизначеною поведінкою.

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

Те, що ви точно не можете зробити:

  • Трансляція між покажчиками функцій різних конвенцій викликів. Ви зіпсуєте стек і в кращому випадку впадете, в гіршому - безшумно досягнете успіху з величезною роззявленою діркою в безпеці. У програмуванні Windows ви часто передаєте вказівники на функції. Win32 очікує , що всі функції зворотного виклику використовувати stdcallугоду про виклики (які макроси CALLBACK, PASCALі WINAPIвсе розширюватися). Якщо ви передасте покажчик функції, який використовує стандартну умову виклику C ( cdecl), це призведе до несправності.
  • У C ++, передавання між покажчиками на функції члена класу та звичайними вказівниками на функції. Це часто піднімає початківців на C ++. Функції-члени класу мають прихований thisпараметр, і якщо ви додасте функцію-член до звичайної функції, немає thisоб’єкта для використання, і знову ж таки, це призведе до великої бідності.

Ще одна погана ідея, яка іноді може спрацювати, але також невизначена поведінка:

  • Трансляція між покажчиками функцій та звичайними вказівниками (наприклад, приведення а void (*)(void)до а void*). Покажчики функцій не обов'язково мають такий самий розмір, як звичайні вказівники, оскільки на деяких архітектурах вони можуть містити додаткову контекстну інформацію. Можливо, це буде нормально працювати на x86, але пам’ятайте, що це невизначена поведінка.

18
Чи не вся суть у void*тому, що вони сумісні з будь-яким іншим покажчиком? Не повинно виникнути проблем з приведенням а struct my_struct*до а void*, насправді вам навіть не потрібно робити кастинг, компілятор повинен просто прийняти його. Наприклад, якщо ви struct my_struct*передаєте a функції, яка приймає a void*, кастинг не потрібен. Чого мені тут не вистачає, що робить їх несумісними?
brianmearns

2
Ця відповідь посилається на "Це, мабуть, буде добре працювати на x86 ...": Чи існують платформи, де це НЕ працюватиме? Хтось має досвід, коли це не вдалося? qsort () для C здається приємним місцем для приведення покажчика функції, якщо це можливо.
kevinarpe

4
@KCArpe: Відповідно до діаграми під заголовком "Впровадження покажчиків на функції членів" у цій статті , 16-розрядний компілятор OpenWatcom іноді використовує більший тип вказівника функції (4 байти), ніж тип вказівника даних (2 байти) у певних конфігураціях. Однак системи, що відповідають POSIX, повинні використовувати те саме представлення, що і void*для типів покажчиків функцій, див . Специфікацію .
Адам Розенфілд,

3
Посилання з @adam тепер посилається на випуск стандарту POSIX 2016 року, де відповідний розділ 2.12.3 було видалено. Ви все ще можете знайти його у виданні 2008 року .
Мартін Тренкманн,

6
@brianmearns Ні, void *є лише "сумісним" з будь-яким іншим (не функціональним) покажчиком дуже точно визначеними способами (які не пов'язані зі значенням стандарту C зі словом "сумісний" у цьому випадку). C дозволяє a void *бути більшим або меншим, ніж a struct my_struct *, або мати біти в іншому порядку, заперечувати або що завгодно. Так void f(void *)і void f(struct my_struct *)може бути ABI-несумісним . C перетворить самі вказівники для вас, якщо це потрібно, але він не буде, а іноді не може перетворити функцію, що вказує, для прийняття можливо іншого типу аргументу.
mtraceur

32

Нещодавно я запитував про це саме те саме питання щодо коду в GLib. (GLib - це основна бібліотека для проекту GNOME і написана на C.) Мені сказали, що вся структура slots'n's сигналів залежить від цього.

В усьому коді є безліч випадків кастингу від типу (1) до (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. 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). Стандарти, певно, прокляті.

Я витратив якийсь час на роздуми над цими відповідями. Ось мій висновок:

  1. Якщо ви пишете бібліотеку зворотного дзвінка, це може бути нормально. Caveat emptor - використовуйте на свій страх і ризик.
  2. В іншому випадку не робіть цього.

Подумавши глибше після написання цієї відповіді, я не здивуюся, якщо код для компіляторів C використовує той самий фокус. І оскільки (більшість / усі?) Сучасні компілятори C завантажені, це означатиме, що фокус безпечний.

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


10
Місце, де це точно не працює, - це компілятор Emscripten LLVM to Javascript. Детальніше див. На github.com/kripken/emscripten/wiki/Asm-pointer-casts .
Бен Лінгс

2
Нове посилання про Emscripten .
ysdx

4
Опубліковане посилання @BenLings буде порушено найближчим часом. Він офіційно перемістився на kripken.github.io/emscripten-site/docs/porting/guidelines/…
Олексій

9

Справа насправді не в тому, чи можете ви. Тривіальним рішенням є

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, лише якщо це дійсно потрібно, і в такому випадку ви були б раді цьому.


Проблема в тому, що це не загальне рішення. Це потрібно робити в кожному конкретному випадку, знаючи функцію. Якщо у вас вже є функція неправильного типу, ви застрягли.
BeeOnRope

Усі компілятори, з якими я це тестував, генеруватимуть код my_callback_helper, якщо він не завжди вбудований. Це однозначно не потрібно, оскільки єдине, що воно схильне робити - це jmp my_callback_function. Компілятор, ймовірно, хоче переконатись, що адреси функцій різні, але, на жаль, він робить це навіть тоді, коли функція позначена C99 inline(тобто "не дбати про адресу").
yyny

Я не впевнений, що це правильно. Інший коментар з іншої відповіді вище (від @mtraceur) говорить, що a void *може бути навіть іншого розміру, ніж a struct *(я думаю, що це неправильно, оскільки в іншому випадку це mallocбуло б порушено, але цей коментар має 5 голосів проти, тому я віддаю йому певний кредит. Якщо @mtraceur має рацію, рішення, яке ви написали, буде неправильним.
cesss,

@cesss: Зовсім не важливо, чи відрізняється розмір. Перетворення на та з void*досі має працювати. Коротше кажучи, void*може бути більше бітів, але якщо ви додасте a struct*до void*цих зайвих бітів, це може бути нулями, і повернення назад може просто відкинути ці нулі знову.
MSalters

@ MSalters: Я справді не знав, чи void *може (теоретично) настільки відрізнятися від struct *. Я реалізую vtable у C, і я використовую thisвказівник на C ++ як перший аргумент для віртуальних функцій. Очевидно, що це thisповинен бути вказівник на "поточну" (похідну) структуру. Отже, віртуальним функціям потрібні різні прототипи залежно від структури, в якій вони реалізовані. Я думав, що використання void *thisаргументу все виправить, але тепер я дізнався, що це невизначена поведінка ...
cesss

6

У вас є сумісний тип функції, якщо тип повернення та типи параметрів сумісні - в основному (це насправді складніше :)). Сумісність така ж, як "той самий тип", просто більш в'яла, щоб мати можливість мати різні типи, але все одно існує певна форма висловлювання "ці типи майже однакові". Наприклад, у C89 дві структури були сумісні, якщо вони інакше були однаковими, але просто їхня назва була іншою. Здається, C99 це змінив. Посилання з обґрунтованого документа (настійно рекомендується прочитати, до речі!):

Оголошення типу структури, об'єднання або перерахування у двох різних одиницях перекладу офіційно не оголошують один і той же тип, навіть якщо текст цих декларацій походить з одного і того ж файлу включення, оскільки одиниці перекладу самі по собі не пов'язані. Таким чином, Стандарт визначає додаткові правила сумісності для таких типів, так що якщо дві такі декларації є досить подібними, вони є сумісними.

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


4

Оскільки код С компілюється до інструкцій, які взагалі не піклуються про типи покажчиків, цілком добре використовувати згаданий вами код. Ви могли б зіткнутися з проблемами, коли запускали 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

В основному, ви можете призначати вказівники на все, що вам подобається, доки дані продовжують мати сенс під час виконання.


0

Якщо ви думаєте про те, як функціонування викликів функціонує в C / C ++, вони штовхають певні елементи у стеці, переходять до нового розташування коду, виконують, а потім повертають стек при поверненні. Якщо ваші покажчики функцій описують функції з однаковим типом повернення та однаковим числом / розміром аргументів, ви повинні бути в порядку.

Таким чином, я думаю, ви повинні бути в змозі зробити це безпечно.


2
ви в безпеці лише до тих пір, поки- structпокажчики та void-показники мають сумісні бітові подання; це не гарантовано
Крістоф

1
Компілятори також можуть передавати аргументи в регістри. І не нечувано використовувати різні регістри для поплавків, інтів чи покажчиків.
MSalters

0

Порожні вказівники сумісні з іншими типами вказівників. Це опора того, як працюють malloc і mem функції ( memcpy, memcmp). Як правило, в C (замість C ++) NULLє макрос, який визначається як ((void *)0).

Подивіться на 6.3.2.3 (Пункт 1) у C99:

Вказівник на void може бути перетворений у або з вказівника на будь-який неповний або тип об'єкта


Це суперечить відповіді Адама Розенфілда , див. Останній абзац та коментарі
користувач

1
Ця відповідь явно неправильна. Будь-який покажчик можна конвертувати у покажчик void, за винятком покажчиків на функції.
marton78
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.