Casting eines Funktionszeigers auf einen anderen Typ

87

Angenommen, ich habe eine Funktion, die einen void (*)(void*)Funktionszeiger zur Verwendung als Rückruf akzeptiert :

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

Nun, wenn ich eine Funktion wie diese habe:

void my_callback_function(struct my_struct* arg);

Kann ich das sicher machen?

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

Ich habe mir diese Frage und einige C-Standards angesehen, die besagen, dass Sie in "kompatible Funktionszeiger" umwandeln können, aber ich kann keine Definition finden, was "kompatibler Funktionszeiger" bedeutet.

Mike Weller
quelle
1
Ich bin ein bisschen ein Neuling, aber was bedeutet ein "void ( ) (void ) Funktionszeiger"?. Ist es ein Zeiger auf eine Funktion, die eine Leere * als Argument akzeptiert und Leere zurückgibt
Digital Gal
2
@Myke: void (*func)(void *)bedeutet, dass dies funcein Zeiger auf eine Funktion mit einer Typensignatur wie z void foo(void *arg). Also ja, du hast recht.
mk12

Antworten:

121

Wenn Sie für den C-Standard einen Funktionszeiger auf einen Funktionszeiger eines anderen Typs umwandeln und diesen dann aufrufen, handelt es sich um ein undefiniertes Verhalten . Siehe Anhang J.2 (informativ):

Das Verhalten ist unter folgenden Umständen undefiniert:

  • Mit einem Zeiger wird eine Funktion aufgerufen, deren Typ nicht mit dem Typ kompatibel ist, auf den gezeigt wird (6.3.2.3).

Abschnitt 6.3.2.3, Absatz 8 lautet:

Ein Zeiger auf eine Funktion eines Typs kann in einen Zeiger auf eine Funktion eines anderen Typs konvertiert werden und wieder zurück; Das Ergebnis muss mit dem ursprünglichen Zeiger verglichen werden. Wenn ein konvertierter Zeiger zum Aufrufen einer Funktion verwendet wird, deren Typ nicht mit dem Typ kompatibel ist, auf den verwiesen wird, ist das Verhalten undefiniert.

Mit anderen Worten, Sie können einen Funktionszeiger auf einen anderen Funktionszeigertyp umwandeln, ihn wieder zurücksetzen und aufrufen, und die Dinge werden funktionieren.

Die Definition von kompatibel ist etwas kompliziert. Es ist in Abschnitt 6.7.5.3, Absatz 15 zu finden:

Damit zwei Funktionstypen kompatibel sind, müssen beide kompatible Rückgabetypen 127 angeben .

Darüber hinaus müssen die Parametertyplisten, sofern beide vorhanden sind, in der Anzahl der Parameter und in der Verwendung des Ellipsen-Terminators übereinstimmen. entsprechende Parameter müssen kompatible Typen haben. Wenn ein Typ eine Parametertypliste hat und der andere Typ von einem Funktionsdeklarator angegeben wird, der nicht Teil einer Funktionsdefinition ist und eine leere Bezeichnerliste enthält, darf die Parameterliste keinen Ellipsenabschluss haben und der Typ jedes Parameters muss mit dem Typ kompatibel sein, der sich aus der Anwendung der Standardargument-Promotions ergibt. Wenn ein Typ eine Parametertypliste hat und der andere Typ durch eine Funktionsdefinition angegeben wird, die eine (möglicherweise leere) Bezeichnerliste enthält, müssen beide in der Anzahl der Parameter übereinstimmen. und der Typ jedes Prototypparameters muss mit dem Typ kompatibel sein, der sich aus der Anwendung der Standardargumentwerbung auf den Typ des entsprechenden Bezeichners ergibt. (Bei der Bestimmung der Typkompatibilität und eines zusammengesetzten Typs wird angenommen, dass jeder mit Funktion oder Array-Typ deklarierte Parameter den angepassten Typ und jeder mit qualifiziertem Typ deklarierte Parameter die nicht qualifizierte Version seines deklarierten Typs hat.)

127) Wenn beide Funktionstypen "alter Stil" sind, werden Parametertypen nicht verglichen.

Die Regeln zum Bestimmen, ob zwei Typen kompatibel sind, sind in Abschnitt 6.2.7 beschrieben, und ich werde sie hier nicht zitieren, da sie ziemlich lang sind, aber Sie können sie im Entwurf des C99-Standards (PDF) lesen .

Die relevante Regel hier ist in Abschnitt 6.7.5.1, Absatz 2:

Damit zwei Zeigertypen kompatibel sind, müssen beide identisch qualifiziert sein und beide müssen Zeiger auf kompatible Typen sein.

Da a void* nicht mit a kompatibel iststruct my_struct* , ist ein Funktionszeiger vom Typ void (*)(void*)nicht mit einem Funktionszeiger vom Typ kompatibel void (*)(struct my_struct*), so dass diese Umwandlung von Funktionszeigern ein technisch undefiniertes Verhalten ist.

In der Praxis können Sie jedoch in einigen Fällen sicher mit Casting-Funktionszeigern davonkommen. In der x86-Aufrufkonvention werden Argumente auf den Stapel übertragen, und alle Zeiger haben dieselbe Größe (4 Byte in x86 oder 8 Byte in x86_64). Das Aufrufen eines Funktionszeigers läuft darauf hinaus, die Argumente auf dem Stapel zu verschieben und einen indirekten Sprung zum Ziel des Funktionszeigers zu machen, und es gibt offensichtlich keine Vorstellung von Typen auf der Ebene des Maschinencodes.

Dinge, die Sie definitiv nicht tun können:

  • Zwischen Funktionszeigern verschiedener Aufrufkonventionen umwandeln. Sie werden den Stapel durcheinander bringen und im besten Fall abstürzen, im schlimmsten Fall lautlos mit einer riesigen Sicherheitslücke klaffen. In der Windows-Programmierung geben Sie häufig Funktionszeiger weiter. Win32 erwartet , dass alle Callback - Funktionen , die verwenden stdcallAufrufkonvention (welche die Makros CALLBACK, PASCALund WINAPIalle erweitern). Wenn Sie einen Funktionszeiger übergeben, der die Standard-C-Aufrufkonvention ( cdecl) verwendet, führt dies zu einer Unrichtigkeit.
  • In C ++ zwischen Funktionselementzeigern und regulären Funktionszeigern umwandeln. Dies löst häufig C ++ - Neulinge aus. Klassenmitgliedsfunktionen haben einen versteckten thisParameter, und wenn Sie eine Elementfunktion in eine reguläre Funktion thisumwandeln , gibt es kein zu verwendendes Objekt, und wiederum führt dies zu einer großen Beeinträchtigung.

Eine andere schlechte Idee, die manchmal funktioniert, aber auch undefiniertes Verhalten ist:

  • Casting zwischen Funktionszeigern und regulären Zeigern (z. B. Casting von a void (*)(void)nach a void*). Funktionszeiger haben nicht unbedingt die gleiche Größe wie normale Zeiger, da sie auf einigen Architekturen möglicherweise zusätzliche Kontextinformationen enthalten. Dies wird unter x86 wahrscheinlich in Ordnung sein, aber denken Sie daran, dass es sich um ein undefiniertes Verhalten handelt.
Adam Rosenfield
quelle
1
@adam, ich sehe, du hast auch deine Nachforschungen angestellt :) chuck, void * und sein Strukturtyp * sind nicht kompatibel. Es ist also undefiniertes Verhalten, es aufzurufen. Trotzdem ist undefiniertes Verhalten nicht immer so schlimm. Wenn der Compiler es gut macht, warum sollten Sie sich darüber Sorgen machen? In diesem Fall gibt es jedoch eine saubere Lösung.
Johannes Schaub - litb
18
Ist der springende Punkt nicht, void*dass sie mit jedem anderen Zeiger kompatibel sind? Es sollte kein Problem geben, a struct my_struct*in a umzuwandeln void*, tatsächlich sollten Sie nicht einmal umwandeln müssen, der Compiler sollte es einfach akzeptieren. Wenn Sie beispielsweise a struct my_struct*an eine Funktion übergeben, die a übernimmt void*, ist kein Casting erforderlich. Was fehlt mir hier, was diese inkompatibel macht?
Brianmearns
3
@KCArpe: Gemäß der Tabelle unter der Überschrift "Implementierungen von Elementfunktionszeigern " in diesem Artikel verwendet der 16-Bit-OpenWatcom-Compiler in bestimmten Konfigurationen manchmal einen größeren Funktionszeigertyp (4 Byte) als den Datenzeigertyp (2 Byte). POSIX-konforme Systeme müssen jedoch dieselbe Darstellung verwenden void*wie für Funktionszeigertypen, siehe Spezifikation .
Adam Rosenfield
3
Der Link von @adam verweist jetzt auf die Ausgabe 2016 des POSIX-Standards, in der der entsprechende Abschnitt 2.12.3 entfernt wurde. Sie finden es immer noch in der Ausgabe 2008 .
Martin Trenkmann
5
@brianmearns Nein, void *ist nur auf sehr genau definierte Weise mit jedem anderen (nicht funktionsfähigen) Zeiger "kompatibel" (was in diesem Fall nichts mit dem zu tun hat, was der C-Standard mit dem Wort "kompatibel" bedeutet). C erlaubt es a void *, größer oder kleiner als a zu sein struct my_struct *oder die Bits in unterschiedlicher Reihenfolge oder negiert zu haben oder was auch immer. Also void f(void *)und void f(struct my_struct *)kann ABI-inkompatibel sein . C konvertiert die Zeiger bei Bedarf selbst für Sie, kann jedoch eine Funktion, auf die verwiesen wird, nicht und manchmal auch nicht konvertieren, um einen möglicherweise anderen Argumenttyp zu verwenden.
mtraceur
32

Ich habe kürzlich nach genau demselben Problem bezüglich eines Codes in GLib gefragt. (GLib ist eine Kernbibliothek für das GNOME-Projekt und in C geschrieben.) Mir wurde gesagt, dass das gesamte Slots'n'signals-Framework davon abhängt.

Im gesamten Code gibt es zahlreiche Fälle von Casting von Typ (1) bis (2):

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

Es ist üblich, bei Anrufen wie diesen eine Kette durchzuketten:

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

Überzeugen Sie sich hier unter g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Die obigen Antworten sind detailliert und wahrscheinlich richtig - wenn Sie im Normungsausschuss sitzen. Adam und Johannes verdienen Anerkennung für ihre gut recherchierten Antworten. In freier Wildbahn werden Sie jedoch feststellen, dass dieser Code einwandfrei funktioniert. Umstritten? Ja. Beachten Sie Folgendes: GLib kompiliert / arbeitet / testet auf einer großen Anzahl von Plattformen (Linux / Solaris / Windows / OS X) mit einer Vielzahl von Compilern / Linkern / Kernel-Loadern (GCC / CLang / MSVC). Standards sind verdammt, denke ich.

Ich habe einige Zeit damit verbracht, über diese Antworten nachzudenken. Hier ist meine Schlussfolgerung:

  1. Wenn Sie eine Rückrufbibliothek schreiben, ist dies möglicherweise in Ordnung. Vorsichtsmaßnahme - Verwendung auf eigenes Risiko.
  2. Sonst tu es nicht.

Wenn ich nach dem Schreiben dieser Antwort genauer nachdenke, wäre ich nicht überrascht, wenn der Code für C-Compiler denselben Trick verwendet. Und da (die meisten / alle?) Moderne C-Compiler gebootet sind, würde dies bedeuten, dass der Trick sicher ist.

Eine wichtigere Frage für die Forschung: Kann jemand eine Plattform / Compiler / Linker / Loader finden, bei der dieser Trick nicht funktioniert? Wichtige Brownie-Punkte für diesen. Ich wette, es gibt einige eingebettete Prozessoren / Systeme, die es nicht mögen. Für Desktop-Computer (und wahrscheinlich für Mobilgeräte / Tablets) funktioniert dieser Trick jedoch wahrscheinlich immer noch.

Kevinarpe
quelle
10
Ein Ort, an dem es definitiv nicht funktioniert, ist der Emscripten LLVM to Javascript Compiler. Weitere Informationen finden Sie unter github.com/kripken/emscripten/wiki/Asm-pointer-casts .
Ben Lings
2
Aktualisierte Referenz über die Emscripten .
ysdx
3
Der Link @BenLings wird in naher Zukunft unterbrochen. Es ist offiziell zu kripken.github.io/emscripten-site/docs/porting/guidelines/… umgezogen
Alex Reinking
9

Der Punkt ist wirklich nicht, ob Sie können. Die triviale Lösung ist

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

Ein guter Compiler generiert nur dann Code für my_callback_helper, wenn er wirklich benötigt wird. In diesem Fall wären Sie froh, dass dies der Fall ist.

MSalters
quelle
Das Problem ist, dass dies keine allgemeine Lösung ist. Dies muss von Fall zu Fall mit Kenntnis der Funktion erfolgen. Wenn Sie bereits eine Funktion des falschen Typs haben, stecken Sie fest.
BeeOnRope
Alle Compiler, mit denen ich dies getestet habe, generieren Code für my_callback_helper, es sei denn, er ist immer inline. Dies ist definitiv nicht notwendig, da es nur dazu neigt jmp my_callback_function. Der Compiler möchte wahrscheinlich sicherstellen, dass die Adressen für die Funktionen unterschiedlich sind, tut dies jedoch leider auch dann, wenn die Funktion mit C99 markiert ist inline(dh "die Adresse ist mir egal").
yyny
6

Sie haben einen kompatiblen Funktionstyp, wenn der Rückgabetyp und die Parametertypen kompatibel sind - im Grunde genommen (in der Realität ist dies komplizierter :)). Die Kompatibilität ist die gleiche wie bei "gleichem Typ", nur lockerer, um unterschiedliche Typen zuzulassen, aber es gibt immer noch die Form "diese Typen sind fast gleich". In C89 waren beispielsweise zwei Strukturen kompatibel, wenn sie ansonsten identisch waren, aber nur ihr Name unterschiedlich war. C99 scheint das geändert zu haben. Zitat aus dem Begründungsdokument (sehr zu empfehlen, übrigens!):

Struktur-, Vereinigungs- oder Aufzählungstypdeklarationen in zwei verschiedenen Übersetzungseinheiten deklarieren formal nicht denselben Typ, selbst wenn der Text dieser Deklarationen aus derselben Include-Datei stammt, da die Übersetzungseinheiten selbst disjunkt sind. Der Standard legt daher zusätzliche Kompatibilitätsregeln für solche Typen fest, sodass zwei solche Erklärungen kompatibel sind, wenn sie ausreichend ähnlich sind.

Das heißt - ja, streng genommen ist dies ein undefiniertes Verhalten, da Ihre do_stuff-Funktion oder eine andere Person Ihre Funktion mit einem Funktionszeiger void*als Parameter aufruft , Ihre Funktion jedoch einen inkompatiblen Parameter hat. Trotzdem erwarte ich von allen Compilern, dass sie es kompilieren und ausführen, ohne zu stöhnen. Sie können jedoch sauberer arbeiten, indem Sie eine andere Funktion verwenden void*(und diese als Rückruffunktion registrieren), die dann nur Ihre eigentliche Funktion aufruft.

Johannes Schaub - litb
quelle
4

Da C-Code zu Anweisungen kompiliert wird, die sich überhaupt nicht um Zeigertypen kümmern, ist es in Ordnung, den von Ihnen erwähnten Code zu verwenden. Sie würden auf Probleme stoßen, wenn Sie do_stuff mit Ihrer Rückruffunktion ausführen und auf etwas anderes als die my_struct-Struktur als Argument verweisen würden.

Ich hoffe, ich kann es klarer machen, indem ich zeige, was nicht funktionieren würde:

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

oder...

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

Grundsätzlich können Sie Zeiger auf beliebige Elemente setzen, solange die Daten zur Laufzeit weiterhin sinnvoll sind.

che
quelle
0

Wenn Sie über die Funktionsweise von Funktionsaufrufen in C / C ++ nachdenken, werden bestimmte Elemente auf dem Stapel verschoben, zum neuen Code-Speicherort gesprungen, ausgeführt und der Stapel bei der Rückkehr eingeblendet. Wenn Ihre Funktionszeiger Funktionen mit demselben Rückgabetyp und derselben Anzahl / Größe von Argumenten beschreiben, sollten Sie in Ordnung sein.

Daher denke ich, dass Sie dies sicher tun sollten.

Steve Rowe
quelle
2
Sie sind nur so lange sicher, wie structZeiger und voidZeiger kompatible Bitdarstellungen haben. Das ist nicht garantiert der Fall
Christoph
1
Compiler können auch Argumente in Registern übergeben. Und es ist nicht ungewöhnlich, unterschiedliche Register für Floats, Ints oder Zeiger zu verwenden.
MSalters
0

Leere Zeiger sind mit anderen Zeigertypen kompatibel. Es ist das Rückgrat der Funktionsweise von malloc und mem ( memcpy( memcmp)). In der Regel ist in C (anstelle von C ++) NULLein Makro definiert als((void *)0) .

Siehe 6.3.2.3 (Punkt 1) in C99:

Ein Zeiger auf void kann in oder von einem Zeiger auf einen unvollständigen oder Objekttyp konvertiert werden

xslogic
quelle
Dies widerspricht der Antwort von Adam Rosenfield , siehe den letzten Absatz und die Kommentare
Benutzer
1
Diese Antwort ist eindeutig falsch. Jeder Zeiger kann in einen ungültigen Zeiger konvertiert werden, mit Ausnahme von Funktionszeigern.
Marton78