Conversion d'un pointeur de fonction vers un autre type


89

Disons que j'ai une fonction qui accepte un void (*)(void*)pointeur de fonction à utiliser comme rappel:

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

Maintenant, si j'ai une fonction comme celle-ci:

void my_callback_function(struct my_struct* arg);

Puis-je faire cela en toute sécurité?

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

J'ai regardé cette question et j'ai examiné certaines normes C qui disent que vous pouvez convertir en «pointeurs de fonction compatibles», mais je ne trouve pas de définition de ce que signifie «pointeur de fonction compatible».


1
Je suis un peu un débutant mais que signifie un " pointeur de fonction void ( ) (void )"? Est-ce un pointeur vers une fonction qui accepte un void * comme argument et renvoie void
Digital Gal

2
@Myke: void (*func)(void *)signifie qu'il funcs'agit d'un pointeur vers une fonction avec une signature de type telle que void foo(void *arg). Alors oui, tu as raison.
mk12

Réponses:


122

En ce qui concerne la norme C, si vous transtypez un pointeur de fonction vers un pointeur de fonction d'un type différent et que vous l'appelez ensuite, il s'agit d' un comportement non défini . Voir l'annexe J.2 (informative):

Le comportement n'est pas défini dans les circonstances suivantes:

  • Un pointeur est utilisé pour appeler une fonction dont le type n'est pas compatible avec le type pointé (6.3.2.3).

Le paragraphe 8 de la section 6.3.2.3 se lit comme suit:

Un pointeur vers une fonction d'un type peut être converti en un pointeur vers une fonction d'un autre type et inversement; le résultat doit être comparé égal au pointeur d'origine. Si un pointeur converti est utilisé pour appeler une fonction dont le type n'est pas compatible avec le type pointé, le comportement n'est pas défini.

Donc, en d'autres termes, vous pouvez convertir un pointeur de fonction en un type de pointeur de fonction différent, le renvoyer à nouveau et l'appeler, et les choses fonctionneront.

La définition de compatible est quelque peu compliquée. Il se trouve dans la section 6.7.5.3, paragraphe 15:

Pour que deux types de fonctions soient compatibles, les deux doivent spécifier des types de retour compatibles 127 .

De plus, les listes de types de paramètres, si les deux sont présentes, doivent correspondre au nombre de paramètres et à l'utilisation du terminateur d'ellipse; les paramètres correspondants doivent avoir des types compatibles. Si un type a une liste de types de paramètres et l'autre type est spécifié par un déclarateur de fonction qui ne fait pas partie d'une définition de fonction et qui contient une liste d'identificateurs vide, la liste de paramètres ne doit pas avoir de terminateur d'ellipse et le type de chaque paramètre doit être compatible avec le type qui résulte de l'application des promotions d'argument par défaut. Si un type a une liste de types de paramètres et l'autre type est spécifié par une définition de fonction qui contient une liste d'identificateurs (éventuellement vide), les deux doivent s'accorder sur le nombre de paramètres, et le type de chaque paramètre prototype doit être compatible avec le type qui résulte de l'application de l'argument par défaut promotions au type de l'identifiant correspondant. (Dans la détermination de la compatibilité de type et d'un type composite, chaque paramètre déclaré avec le type de fonction ou de tableau est considéré comme ayant le type ajusté et chaque paramètre déclaré avec le type qualifié est considéré comme ayant la version non qualifiée de son type déclaré.)

127) Si les deux types de fonctions sont de «l'ancien style», les types de paramètres ne sont pas comparés.

Les règles pour déterminer si deux types sont compatibles sont décrites dans la section 6.2.7, et je ne les citerai pas ici car elles sont assez longues, mais vous pouvez les lire sur le projet de norme C99 (PDF) .

La règle pertinente ici est dans la section 6.7.5.1, paragraphe 2:

Pour que deux types de pointeurs soient compatibles, les deux doivent être qualifiés de la même manière et les deux doivent être des pointeurs vers des types compatibles.

Par conséquent, comme a void* n'est pas compatible avec a struct my_struct*, un pointeur de fonction de type void (*)(void*)n'est pas compatible avec un pointeur de fonction de type void (*)(struct my_struct*), donc cette conversion de pointeurs de fonction est un comportement techniquement indéfini.

Dans la pratique, cependant, vous pouvez vous en tirer en toute sécurité avec des pointeurs de fonction de diffusion dans certains cas. Dans la convention d'appel x86, les arguments sont poussés sur la pile et tous les pointeurs ont la même taille (4 octets en x86 ou 8 octets en x86_64). Appeler un pointeur de fonction revient à pousser les arguments sur la pile et à faire un saut indirect vers la cible du pointeur de fonction, et il n'y a évidemment pas de notion de types au niveau du code machine.

Ce que vous ne pouvez certainement pas faire:

  • Conversion entre les pointeurs de fonction de différentes conventions d'appel. Vous allez gâcher la pile et, au mieux, vous écraser, au pire, réussir silencieusement avec un énorme trou de sécurité béant. Dans la programmation Windows, vous passez souvent des pointeurs de fonction. Win32 attend toutes les fonctions de rappel à utiliser la stdcallconvention d' appel (que les macros CALLBACK, PASCALet WINAPItout développer à). Si vous passez un pointeur de fonction qui utilise la convention d'appel standard C ( cdecl), il en résultera un mauvais résultat.
  • En C ++, transtyper entre les pointeurs de fonction de membre de classe et les pointeurs de fonction standard. Cela trébuche souvent les débutants C ++. Les fonctions membres de classe ont un thisparamètre masqué , et si vous transtypez une fonction membre en fonction régulière, il n'y a pas d' thisobjet à utiliser, et encore une fois, il en résultera beaucoup de mauvais.

Une autre mauvaise idée qui peut parfois fonctionner mais qui est également un comportement indéfini:

  • Conversion entre les pointeurs de fonction et les pointeurs réguliers (par exemple, conversion de a void (*)(void)en a void*). Les pointeurs de fonction ne sont pas nécessairement de la même taille que les pointeurs réguliers, car sur certaines architectures, ils peuvent contenir des informations contextuelles supplémentaires. Cela fonctionnera probablement bien sur x86, mais rappelez-vous que ce comportement n'est pas défini.

18
N'est-ce pas void*qu'ils sont compatibles avec n'importe quel autre pointeur? Il ne devrait y avoir aucun problème à convertir a struct my_struct*en a void*, en fait, vous ne devriez même pas avoir à lancer, le compilateur devrait simplement l'accepter. Par exemple, si vous passez a struct my_struct*à une fonction qui prend a void*, aucune conversion n'est requise. Qu'est-ce que je manque ici qui les rend incompatibles?
brianmearns

2
Cette réponse fait référence à "Cela fonctionnera probablement bien sur x86 ...": Y a-t-il des plates-formes où cela ne fonctionnera PAS? Quelqu'un a-t-il de l'expérience lorsque cela a échoué? qsort () pour C semble être un bon endroit pour lancer un pointeur de fonction si possible.
kevinarpe

4
@KCArpe: selon le graphique sous l'en-tête «Implémentations des pointeurs de fonction membre» dans cet article , le compilateur OpenWatcom 16 bits utilise parfois un type de pointeur de fonction plus grand (4 octets) que le type pointeur de données (2 octets) dans certaines configurations. Cependant, les systèmes conformes à POSIX doivent utiliser la même représentation pour void*que pour les types de pointeurs de fonction, voir la spécification .
Adam Rosenfield

3
Le lien de @adam fait désormais référence à l'édition 2016 du standard POSIX où la section 2.12.3 correspondante a été supprimée. Vous pouvez toujours le trouver dans l' édition 2008 .
Martin Trenkmann

6
@brianmearns Non, void *n'est "compatible avec" que tout autre pointeur (non fonctionnel) de manière très précise (qui n'est pas liée à ce que le standard C signifie avec le mot "compatible" dans ce cas). C permet void *à a d'être plus grand ou plus petit que a struct my_struct *, ou d'avoir les bits dans un ordre différent ou inversé ou autre. Donc void f(void *)et void f(struct my_struct *)peut être incompatible avec ABI . C convertira les pointeurs eux-mêmes pour vous si nécessaire, mais il ne peut pas et parfois ne peut pas convertir une fonction pointée pour prendre un type d'argument éventuellement différent.
mtraceur

32

J'ai récemment posé des questions sur ce même problème concernant du code dans GLib. (GLib est une bibliothèque principale pour le projet GNOME et écrite en C.) On m'a dit que tout le framework slots'n'signals en dépendait.

Tout au long du code, il existe de nombreuses instances de conversion du type (1) vers (2):

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

Il est courant de passer en chaîne avec des appels comme celui-ci:

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

Voyez par vous-même ici sur g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Les réponses ci-dessus sont détaillées et probablement correctes - si vous faites partie du comité des normes. Adam et Johannes méritent d'être félicités pour leurs réponses bien documentées. Cependant, dans la nature, vous trouverez que ce code fonctionne très bien. Controversé? Oui. Considérez ceci: GLib compile / travaille / teste sur un grand nombre de plates-formes (Linux / Solaris / Windows / OS X) avec une grande variété de compilateurs / linkers / chargeurs de noyau (GCC / CLang / MSVC). Les normes soient damnées, je suppose.

J'ai passé du temps à réfléchir à ces réponses. Voici ma conclusion:

  1. Si vous écrivez une bibliothèque de rappel, cela peut être OK. Caveat emptor - utilisez à vos propres risques.
  2. Sinon, ne le faites pas.

En réfléchissant plus profondément après avoir écrit cette réponse, je ne serais pas surpris si le code des compilateurs C utilise cette même astuce. Et puisque (la plupart / tous?) Les compilateurs C modernes sont bootstrapés, cela impliquerait que l'astuce est sûre.

Une question plus importante à rechercher: quelqu'un peut-il trouver une plate-forme / compilateur / éditeur de liens / chargeur où cette astuce ne fonctionne pas ? Points de brownie majeurs pour celui-là. Je parie que certains processeurs / systèmes embarqués n'aiment pas ça. Cependant, pour l'informatique de bureau (et probablement mobile / tablette), cette astuce fonctionne probablement toujours.


10
Un endroit où cela ne fonctionne certainement pas est le compilateur Emscripten LLVM to Javascript. Voir github.com/kripken/emscripten/wiki/Asm-pointer-casts pour plus de détails.
Ben Lings

2
Référence mise à jour sur Emscripten .
ysdx

4
Le lien @BenLings publié sera rompu dans un proche avenir. Il a officiellement été déplacé vers kripken.github.io/emscripten-site/docs/porting/guidelines/…
Alex Reinking

9

Le point n'est vraiment pas de savoir si vous pouvez. La solution triviale est

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

Un bon compilateur ne générera du code pour my_callback_helper que si c'est vraiment nécessaire, auquel cas vous seriez heureux de l'avoir fait.


Le problème est que ce n'est pas une solution générale. Cela doit être fait au cas par cas avec connaissance de la fonction. Si vous avez déjà une fonction du mauvais type, vous êtes bloqué.
BeeOnRope

Tous les compilateurs avec lesquels j'ai testé cela génèrent du code pour my_callback_helper, à moins qu'il ne soit toujours en ligne. Ce n'est certainement pas nécessaire, car la seule chose que cela a tendance à faire est jmp my_callback_function. Le compilateur veut probablement s'assurer que les adresses des fonctions sont différentes, mais malheureusement, il le fait même lorsque la fonction est marquée avec C99 inline(c'est-à-dire "ne se soucie pas de l'adresse").
yyny le

Je ne suis pas sûr que ce soit correct. Un autre commentaire d'une autre réponse ci-dessus (par @mtraceur) dit que a void *peut même être de taille différente de a struct *(je pense que c'est faux, car autrement mallocserait cassé, mais ce commentaire a 5 votes positifs, donc je lui donne un peu de crédit. Si @mtraceur a raison, la solution que vous avez écrite ne serait pas correcte.
cesse le

@cesss: Peu importe si la taille est différente. La conversion vers et depuis void*doit encore fonctionner. En bref, il void*peut y avoir plus de bits, mais si vous lancez un struct*sur void*ces bits supplémentaires, il peut s'agir de zéros et le renvoi peut simplement annuler ces zéros à nouveau.
MSalters le

@MSalters: Je ne savais vraiment pas qu'un void *pouvait (en théorie) être si différent d'un struct *. J'implémente une vtable en C et j'utilise un thispointeur C ++ - ish comme premier argument des fonctions virtuelles. De toute évidence, thisdoit être un pointeur vers la structure "courante" (dérivée). Ainsi, les fonctions virtuelles ont besoin de différents prototypes en fonction de la structure dans laquelle elles sont implémentées. Je pensais que l'utilisation d'un void *thisargument résoudrait tout, mais maintenant j'ai appris que son comportement n'est pas défini ...
cesses le

6

Vous avez un type de fonction compatible si le type de retour et les types de paramètres sont compatibles - en gros (c'est plus compliqué en réalité :)). La compatibilité est la même que celle du "même type", juste plus laxiste pour permettre d'avoir des types différents tout en ayant une certaine forme de dire "ces types sont presque les mêmes". Dans C89, par exemple, deux structures étaient compatibles si elles étaient par ailleurs identiques mais leur nom était différent. C99 semble avoir changé cela. Citant le document de justification c (lecture hautement recommandée, d'ailleurs!):

Les déclarations de type structure, union ou énumération dans deux unités de traduction différentes ne déclarent pas formellement le même type, même si le texte de ces déclarations provient du même fichier d'inclusion, puisque les unités de traduction sont elles-mêmes disjointes. La norme spécifie ainsi des règles de compatibilité supplémentaires pour ces types, de sorte que si deux de ces déclarations sont suffisamment similaires, elles sont compatibles.

Cela dit - oui, c'est strictement un comportement indéfini, car votre fonction do_stuff ou quelqu'un d'autre appellera votre fonction avec un pointeur de fonction ayant void*comme paramètre, mais votre fonction a un paramètre incompatible. Mais néanmoins, je m'attends à ce que tous les compilateurs le compilent et l'exécutent sans gémir. Mais vous pouvez faire un nettoyage en ayant une autre fonction prenant un void*(et en l'enregistrant comme fonction de rappel) qui appellera alors votre fonction réelle.


4

Comme le code C se compile en instructions qui ne se soucient pas du tout des types de pointeurs, il est tout à fait correct d'utiliser le code que vous mentionnez. Vous rencontriez des problèmes lorsque vous exécutiez do_stuff avec votre fonction de rappel et votre pointeur vers autre chose que la structure my_struct comme argument.

J'espère pouvoir clarifier les choses en montrant ce qui ne fonctionnerait pas:

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

ou...

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

Fondamentalement, vous pouvez lancer des pointeurs sur ce que vous voulez, tant que les données continuent à avoir un sens au moment de l'exécution.


0

Si vous pensez à la façon dont les appels de fonction fonctionnent en C / C ++, ils poussent certains éléments sur la pile, sautent au nouvel emplacement de code, s'exécutent, puis font apparaître la pile au retour. Si vos pointeurs de fonction décrivent des fonctions avec le même type de retour et le même nombre / taille d'arguments, vous devriez être d'accord.

Ainsi, je pense que vous devriez pouvoir le faire en toute sécurité.


2
vous êtes en sécurité uniquement tant que struct-pointers et void-pointers ont des représentations de bits compatibles; ce n'est pas garanti que ce soit le cas
Christoph

1
Les compilateurs peuvent également passer des arguments dans les registres. Et il n'est pas rare d'utiliser différents registres pour les flottants, les entiers ou les pointeurs.
MSalters

0

Les pointeurs vides sont compatibles avec d'autres types de pointeurs. C'est l'épine dorsale du fonctionnement de malloc et des fonctions mem ( memcpy, memcmp). En règle générale, en C (plutôt qu'en C ++) NULLest une macro définie comme ((void *)0).

Regardez 6.3.2.3 (Item 1) dans C99:

Un pointeur vers void peut être converti vers ou à partir d'un pointeur vers tout type d'objet ou incomplet


Cela contredit la réponse d'Adam Rosenfield , voir le dernier paragraphe et les commentaires
utilisateur

1
Cette réponse est clairement erronée. Tout pointeur est convertible en pointeur vide, à l' exception des pointeurs de fonction.
marton78
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.