Rzutowanie wskaźnika funkcji na inny typ


89

Powiedzmy, że mam funkcję, która akceptuje void (*)(void*)wskaźnik funkcji do użycia jako wywołanie zwrotne:

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

Teraz, jeśli mam taką funkcję:

void my_callback_function(struct my_struct* arg);

Czy mogę to zrobić bezpiecznie?

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

Przyjrzałem się temu pytaniu i przyjrzałem się niektórym standardom C, które mówią, że można rzutować na „zgodne wskaźniki funkcji”, ale nie mogę znaleźć definicji tego, co oznacza „zgodny wskaźnik funkcji”.


1
Jestem trochę nowicjuszem, ale co oznacza „ wskaźnik funkcji void ( ) (void )”? Czy jest to wskaźnik do funkcji, która akceptuje void * jako argument i zwraca void
Digital Gal

2
@Myke: void (*func)(void *)oznacza, że funcjest to wskaźnik do funkcji z podpisem typu, takim jak void foo(void *arg). Więc tak, masz rację.
mk12

Odpowiedzi:


122

Jeśli chodzi o standard C, rzutowanie wskaźnika funkcji na wskaźnik funkcji innego typu, a następnie wywołanie go, oznacza niezdefiniowane zachowanie . Patrz załącznik J.2 (informacyjny):

Zachowanie jest niezdefiniowane w następujących okolicznościach:

  • Wskaźnik służy do wywołania funkcji, której typ nie jest zgodny z typem wskazywanym (6.3.2.3).

Sekcja 6.3.2.3, akapit 8 brzmi:

Wskaźnik do funkcji jednego typu można przekształcić we wskaźnik do funkcji innego typu iz powrotem; wynik jest równy pierwotnemu wskaźnikowi. Jeśli przekonwertowany wskaźnik jest używany do wywołania funkcji, której typ nie jest zgodny z typem wskazanym, zachowanie jest niezdefiniowane.

Innymi słowy, możesz rzutować wskaźnik funkcji na inny typ wskaźnika funkcji, rzutować go z powrotem i wywoływać, a wszystko będzie działać.

Definicja zgodności jest nieco skomplikowana. Można go znaleźć w sekcji 6.7.5.3, akapit 15:

Aby dwa typy funkcji były zgodne, oba powinny określać zgodne typy zwracane 127 .

Ponadto listy typów parametrów, jeśli są obecne, powinny zgadzać się co do liczby parametrów i użycia terminatora wielokropka; odpowiednie parametry mają zgodne typy. Jeśli jeden typ ma listę typów parametrów, a drugi typ jest określony przez deklarator funkcji, który nie jest częścią definicji funkcji i który zawiera pustą listę identyfikatorów, lista parametrów nie powinna mieć zakończenia wielokropka, a typ każdego parametru powinien być zgodne z typem wynikającym z zastosowania domyślnych promocji argumentów. Jeśli jeden typ ma listę typów parametrów, a drugi typ jest określony przez definicję funkcji zawierającą (prawdopodobnie pustą) listę identyfikatorów, oba powinny się zgadzać co do liczby parametrów, a typ każdego parametru prototypu będzie zgodny z typem wynikającym z zastosowania promocji argumentów domyślnych do typu odpowiedniego identyfikatora. (Przy określaniu zgodności typu i typu złożonego każdy parametr zadeklarowany z typem funkcji lub tablicy jest traktowany jako mający dostosowany typ, a każdy parametr zadeklarowany z typem kwalifikowanym jest traktowany jako mający niekwalifikowaną wersję swojego zadeklarowanego typu).

127) Jeśli oba typy funkcji są w „starym stylu”, typy parametrów nie są porównywane.

Zasady określania, czy dwa typy są kompatybilne, są opisane w sekcji 6.2.7 i nie będę ich tutaj cytować, ponieważ są dość obszerne, ale można je przeczytać w wersji roboczej standardu C99 (PDF) .

Odpowiedni przepis znajduje się w sekcji 6.7.5.1, ustęp 2:

Aby dwa typy wskaźników były kompatybilne, oba powinny być identycznie kwalifikowane i oba powinny być wskaźnikami do kompatybilnych typów.

W związku z tym, ponieważ a void* nie jest kompatybilny z a struct my_struct*, wskaźnik funkcji typu void (*)(void*)nie jest zgodny ze wskaźnikiem funkcji typu void (*)(struct my_struct*), więc rzutowanie wskaźników funkcji jest technicznie niezdefiniowanym zachowaniem.

W praktyce jednak w niektórych przypadkach można bezpiecznie uciec od rzutowania wskaźników funkcji. W konwencji wywoływania x86 argumenty są umieszczane na stosie, a wszystkie wskaźniki mają ten sam rozmiar (4 bajty w x86 lub 8 bajtów w x86_64). Wywołanie wskaźnika funkcji sprowadza się do umieszczenia argumentów na stosie i wykonania pośredniego skoku do celu wskaźnika funkcji, a na poziomie kodu maszynowego nie ma oczywiście pojęcia o typach.

Rzeczy, których zdecydowanie nie możesz zrobić:

  • Rzutowanie między wskaźnikami funkcji o różnych konwencjach wywoływania. Zepsujesz stos iw najlepszym wypadku rozbijesz się po cichu z wielką, ziejącą luką w zabezpieczeniach. W programowaniu w systemie Windows często przekazuje się wskaźniki funkcji. Win32 oczekuje, że wszystkie funkcje zwrotne używać stdcallkonwencji wzywającą (który makra CALLBACK, PASCALi WINAPIwszystko rozwijać się). Jeśli przekażesz wskaźnik do funkcji, który używa standardowej konwencji wywoływania języka C ( cdecl), wyniknie to źle.
  • W C ++ rzutowanie między wskaźnikami funkcji składowych klasy a zwykłymi wskaźnikami funkcji. To często wyzwala początkujących C ++. Funkcje składowe klasy mają ukryty thisparametr, a jeśli rzutujesz funkcję składową na zwykłą funkcję, nie ma thisobiektu do użycia i znowu spowoduje to wiele zła.

Kolejny zły pomysł, który czasami może działać, ale jest także niezdefiniowanym zachowaniem:

  • Rzutowanie między wskaźnikami funkcji a zwykłymi wskaźnikami (np. Rzutowanie a void (*)(void)do a void*). Wskaźniki funkcji niekoniecznie są tego samego rozmiaru co zwykłe wskaźniki, ponieważ na niektórych architekturach mogą zawierać dodatkowe informacje kontekstowe. Prawdopodobnie zadziała to dobrze na x86, ale pamiętaj, że jest to niezdefiniowane zachowanie.

18
Czy nie chodzi o void*to, że są one zgodne z każdym innym wskaźnikiem? Nie powinno być problemu z rzutowaniem a struct my_struct*na a void*, w rzeczywistości nie powinno to być nawet konieczne, kompilator powinien po prostu to zaakceptować. Na przykład, jeśli przekażesz a struct my_struct*do funkcji, która przyjmuje a void*, rzutowanie nie jest wymagane. Czego tu brakuje, co sprawia, że ​​są one niekompatybilne?
brianmearns

2
Ta odpowiedź odnosi się do „To prawdopodobnie będzie działać poprawnie na x86 ...”: Czy są jakieś platformy, na których to NIE zadziała? Czy ktoś ma doświadczenie, kiedy to się nie powiodło? qsort () dla C wydaje się być dobrym miejscem do rzutowania wskaźnika funkcji, jeśli to możliwe.
kevinarpe

4
@KCArpe: Zgodnie z wykresem pod nagłówkiem „Implementacje wskaźników funkcji składowych” w tym artykule , 16-bitowy kompilator OpenWatcom czasami używa większego typu wskaźnika funkcji (4 bajty) niż typ wskaźnika danych (2 bajty) w niektórych konfiguracjach. Jednak systemy zgodne z POSIX muszą używać tej samej reprezentacji dla void*typów wskaźników funkcji, patrz specyfikacja .
Adam Rosenfield

3
Odnośnik z @adam odnosi się teraz do wydania standardu POSIX 2016, w którym usunięto odpowiednią sekcję 2.12.3. Nadal można go znaleźć w edycji 2008 .
Martin Trenkmann

6
@brianmearns Nie, void *jest tylko „kompatybilny” z każdym innym (niefunkcjonalnym) wskaźnikiem w bardzo precyzyjnie zdefiniowany sposób (który nie ma związku z tym, co standard C oznacza ze słowem „kompatybilny” w tym przypadku). C pozwala a void *być większe lub mniejsze od a struct my_struct *, albo mieć bity w innej kolejności, zanegowane lub cokolwiek innego. Więc void f(void *)i void f(struct my_struct *)może być niezgodne z ABI . W razie potrzeby C sam skonwertuje wskaźniki, ale nie zrobi tego i czasami nie może przekonwertować wskazanej funkcji na inny typ argumentu.
mtraceur

32

Ostatnio zapytałem o dokładnie ten sam problem dotyczący jakiegoś kodu w GLib. (GLib jest podstawową biblioteką projektu GNOME i napisaną w C.) Powiedziano mi, że cały szkielet slots'n'signals zależy od tego.

W całym kodzie występuje wiele przypadków rzutowania z typu (1) do (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

Typowe jest łączenie w łańcuch z takimi wywołaniami:

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

Przekonaj się tutaj g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Powyższe odpowiedzi są szczegółowe i prawdopodobnie poprawne - jeśli zasiadasz w komitecie normalizacyjnym. Adam i Johannes zasługują na uznanie za dobrze zbadane odpowiedzi. Jednak w środowisku naturalnym ten kod działa dobrze. Kontrowersyjny? Tak. Rozważ to: GLib kompiluje / działa / testuje na dużej liczbie platform (Linux / Solaris / Windows / OS X) z szeroką gamą kompilatorów / konsolidatorów / ładujących jądra (GCC / CLang / MSVC). Chyba standardy.

Spędziłem trochę czasu, zastanawiając się nad tymi odpowiedziami. Oto mój wniosek:

  1. Jeśli piszesz bibliotekę wywołań zwrotnych, może to być w porządku. Caveat emptor - używaj na własne ryzyko.
  2. W przeciwnym razie nie rób tego.

Zastanawiając się głębiej po napisaniu tej odpowiedzi, nie zdziwiłbym się, gdyby kod dla kompilatorów C używa tej samej sztuczki. A ponieważ (większość / wszystkie?) Współczesne kompilatory C są ładowane, sugerowałoby to, że sztuczka jest bezpieczna.

Ważniejsze pytanie do zbadania: Czy ktoś może znaleźć platformę / kompilator / konsolidator / ładowacz, na którym ta sztuczka nie działa? Duże punkty za ciastko. Założę się, że jest kilka wbudowanych procesorów / systemów, które tego nie lubią. Jednak w przypadku komputerów stacjonarnych (i prawdopodobnie telefonu komórkowego / tabletu) ta sztuczka prawdopodobnie nadal działa.


10
Miejscem, w którym zdecydowanie nie działa, jest kompilator Emscripten LLVM do Javascript. Szczegółowe informacje można znaleźć na github.com/kripken/emscripten/wiki/Asm-pointer-casts .
Ben Lings,

2
Zaktualizowana referencja o Emscripten .
ysdx

4
Link opublikowany przez @BenLings zostanie wkrótce przerwany. Oficjalnie został przeniesiony na kripken.github.io/emscripten-site/docs/porting/guidelines/…
Alex Reinking

9

Tak naprawdę nie chodzi o to, czy możesz. Banalne rozwiązanie jest takie

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

Dobry kompilator wygeneruje kod dla my_callback_helper tylko wtedy, gdy jest naprawdę potrzebny, w takim przypadku byłbyś zadowolony, że to zrobił.


Problem w tym, że to nie jest rozwiązanie ogólne. Należy to zrobić indywidualnie dla każdego przypadku ze znajomością funkcji. Jeśli masz już funkcję niewłaściwego typu, utkniesz.
BeeOnRope

Wszystkie kompilatory, z którymi to testowałem, będą generować kod my_callback_helper, chyba że jest on zawsze wbudowany. To zdecydowanie nie jest konieczne, ponieważ jedyne, co zwykle robi, to jmp my_callback_function. Kompilator prawdopodobnie chce się upewnić, że adresy funkcji są różne, ale niestety robi to nawet wtedy, gdy funkcja jest oznaczona C99 inline(tj. „Nie obchodzi mnie adres”).
yyny

Nie jestem pewien, czy to prawda. Inny komentarz z innej odpowiedzi powyżej (autorstwa @mtraceur) mówi, że a void *może być nawet innego rozmiaru niż a struct *(myślę, że to źle, ponieważ w przeciwnym razie mallocbyłby zepsuty, ale ten komentarz ma 5 głosów za, więc przypisuję mu trochę uznania. Jeśli @mtraceur ma rację, rozwiązanie, które napisałeś, nie byłoby poprawne
kończy

@ Dostęp: nie ma znaczenia, czy rozmiar jest inny. Konwersja do i z void*nadal musi działać. Krótko mówiąc, void*może mieć więcej bitów, ale jeśli rzucisz a struct*na void*te dodatkowe bity, mogą to być zera, a odrzucenie może po prostu ponownie odrzucić te zera.
MSalters

@MSalters: Naprawdę nie wiedziałem, void *że (teoretycznie) może być tak różny od struct *. Implementuję tabelę vtable w C i używam wskaźnika C ++ - ish thisjako pierwszego argumentu funkcji wirtualnych. Oczywiście thismusi być wskaźnikiem do „bieżącej” (pochodnej) struktury. Tak więc funkcje wirtualne wymagają różnych prototypów w zależności od struktury, w której są zaimplementowane. Myślałem, że użycie void *thisargumentu naprawi wszystko, ale teraz dowiedziałem się, że jego zachowanie jest nieokreślone ...
cesss

6

Masz zgodny typ funkcji, jeśli typ zwracany i typy parametrów są zgodne - w zasadzie (w rzeczywistości jest to bardziej skomplikowane :)). Zgodność jest tym samym, co „ten sam typ”, tylko bardziej luźna, aby pozwolić na różne typy, ale nadal istnieje pewna forma powiedzenia „te typy są prawie takie same”. Na przykład w C89 dwie struktury były kompatybilne, jeśli poza tym były identyczne, ale tylko ich nazwa była inna. Wydaje się, że C99 to zmieniło. Cytowanie z dokumentu uzasadniającego (przy okazji bardzo polecana lektura!):

Deklaracje typu struktury, unii lub wyliczenia w dwóch różnych jednostkach tłumaczeniowych nie deklarują formalnie tego samego typu, nawet jeśli tekst tych deklaracji pochodzi z tego samego pliku dołączanego, ponieważ jednostki tłumaczeniowe są rozłączne. Norma określa zatem dodatkowe zasady kompatybilności dla takich typów, tak że jeśli dwie takie deklaracje są dostatecznie podobne, są zgodne.

To powiedziawszy - tak, ściśle jest to niezdefiniowane zachowanie, ponieważ twoja funkcja do_stuff lub ktoś inny wywoła twoją funkcję ze wskaźnikiem funkcji mającym void*jako parametr, ale twoja funkcja ma niekompatybilny parametr. Niemniej jednak oczekuję, że wszystkie kompilatory skompilują i uruchomią go bez narzekania. Ale możesz zrobić to lepiej, mając inną funkcję, która pobiera void*(i rejestruje to jako funkcję zwrotną), która wtedy po prostu wywoła twoją rzeczywistą funkcję.


4

Ponieważ kod C kompiluje się do instrukcji, które w ogóle nie dbają o typy wskaźników, użycie wspomnianego kodu jest całkiem w porządku. Możesz napotkać problemy, gdy uruchomisz do_stuff ze swoją funkcją zwrotną i wskaźnikiem do czegoś innego niż struktura my_struct jako argument.

Mam nadzieję, że uda mi się to wyjaśnić, pokazując, co by nie zadziałało:

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

lub...

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

Zasadniczo możesz rzutować wskaźniki na cokolwiek chcesz, o ile dane nadal mają sens w czasie wykonywania.


0

Jeśli myślisz o sposobie działania wywołań funkcji w C / C ++, umieszczają one pewne elementy na stosie, przeskakują do nowej lokalizacji kodu, wykonują, a następnie zdejmują stos po powrocie. Jeśli wskaźniki funkcji opisują funkcje o tym samym zwracanym typie i tej samej liczbie / rozmiarze argumentów, wszystko powinno być w porządku.

Dlatego uważam, że powinieneś być w stanie to zrobić bezpiecznie.


2
jesteś bezpieczny tylko wtedy, gdy struct-pointers i void-pointers mają zgodne reprezentacje bitów; to nie jest gwarantowane
Christoph,

1
Kompilatory mogą również przekazywać argumenty w rejestrach. I nie jest niczym niezwykłym używanie różnych rejestrów dla liczb zmiennoprzecinkowych, liczb całkowitych lub wskaźników.
MSalters

0

Wskaźniki pustki są zgodne z innymi typami wskaźników. Jest to podstawa działania malloc i funkcji mem ( memcpy, memcmp). Zazwyczaj w języku C (zamiast C ++) NULLjest makro zdefiniowane jako ((void *)0).

Spójrz na 6.3.2.3 (pozycja 1) w C99:

Wskaźnik do void można przekonwertować na lub ze wskaźnika do dowolnego typu niekompletnego lub obiektowego


Jest to sprzeczne z odpowiedzią Adama Rosenfielda , zobacz ostatni akapit i komentarze
użytkownik

1
Ta odpowiedź jest ewidentnie błędna. Dowolny wskaźnik można zamienić na wskaźnik void, z wyjątkiem wskaźników funkcji.
marton78
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.