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”.
c
function-pointers
Mike Weller
źródło
źródło
void (*func)(void *)
oznacza, żefunc
jest to wskaźnik do funkcji z podpisem typu, takim jakvoid foo(void *arg)
. Więc tak, masz rację.Odpowiedzi:
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):
Sekcja 6.3.2.3, akapit 8 brzmi:
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:
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:
W związku z tym, ponieważ a
void*
nie jest kompatybilny z astruct my_struct*
, wskaźnik funkcji typuvoid (*)(void*)
nie jest zgodny ze wskaźnikiem funkcji typuvoid (*)(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ć:
stdcall
konwencji wzywającą (który makraCALLBACK
,PASCAL
iWINAPI
wszystko 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.this
parametr, a jeśli rzutujesz funkcję składową na zwykłą funkcję, nie mathis
obiektu 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:
void (*)(void)
do avoid*
). 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.źródło
void*
to, że są one zgodne z każdym innym wskaźnikiem? Nie powinno być problemu z rzutowaniem astruct my_struct*
na avoid*
, w rzeczywistości nie powinno to być nawet konieczne, kompilator powinien po prostu to zaakceptować. Na przykład, jeśli przekażesz astruct my_struct*
do funkcji, która przyjmuje avoid*
, rzutowanie nie jest wymagane. Czego tu brakuje, co sprawia, że są one niekompatybilne?void*
typów wskaźników funkcji, patrz specyfikacja .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 avoid *
być większe lub mniejsze od astruct my_struct *
, albo mieć bity w innej kolejności, zanegowane lub cokolwiek innego. Więcvoid f(void *)
ivoid 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.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):
typedef int (*CompareFunc) (const void *a, const void *b)
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.cPowyż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:
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.
źródło
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ł.
źródło
my_callback_helper
, chyba że jest on zawsze wbudowany. To zdecydowanie nie jest konieczne, ponieważ jedyne, co zwykle robi, tojmp my_callback_function
. Kompilator prawdopodobnie chce się upewnić, że adresy funkcji są różne, ale niestety robi to nawet wtedy, gdy funkcja jest oznaczona C99inline
(tj. „Nie obchodzi mnie adres”).void *
może być nawet innego rozmiaru niż astruct *
(myślę, że to źle, ponieważ w przeciwnym raziemalloc
był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 poprawnevoid*
nadal musi działać. Krótko mówiąc,void*
może mieć więcej bitów, ale jeśli rzucisz astruct*
navoid*
te dodatkowe bity, mogą to być zera, a odrzucenie może po prostu ponownie odrzucić te zera.void *
że (teoretycznie) może być tak różny odstruct *
. Implementuję tabelę vtable w C i używam wskaźnika C ++ - ishthis
jako pierwszego argumentu funkcji wirtualnych. Oczywiściethis
musi 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życievoid *this
argumentu naprawi wszystko, ale teraz dowiedziałem się, że jego zachowanie jest nieokreślone ...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!):
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 pobieravoid*
(i rejestruje to jako funkcję zwrotną), która wtedy po prostu wywoła twoją rzeczywistą funkcję.źródło
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.
źródło
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.
źródło
struct
-pointers ivoid
-pointers mają zgodne reprezentacje bitów; to nie jest gwarantowaneWskaź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 ++)NULL
jest makro zdefiniowane jako((void *)0)
.Spójrz na 6.3.2.3 (pozycja 1) w C99:
źródło