web-dev-qa-db-fra.com

Transformer un pointeur de fonction en un autre type

Disons que j'ai une fonction qui accepte un pointeur de fonction void (*)(void*) à 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 le faire en toute sécurité?

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

J'ai regardé cette question et j'ai regardé 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".

77
Mike Weller

En ce qui concerne la norme C, si vous convertissez un pointeur de fonction en un pointeur de fonction d'un type différent, puis appelez cela, c'est comportement indé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).

La section 6.3.2.3, paragraphe 8, se lit comme suit:

Un pointeur vers une fonction d'un type peut être converti en pointeur vers une fonction d'un autre type et inversement; le résultat doit être é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.

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

La définition de compatible est quelque peu compliquée. Il peut être trouvé 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 compatibles127.

De plus, les listes de types de paramètres, si les deux sont présentes, doivent convenir du nombre de paramètres et de l'utilisation du terminateur Ellipsis; les paramètres correspondants doivent avoir des types compatibles. Si un type a une liste de types de paramètres et que l'autre type est spécifié par un déclarant 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 terminaison Ellipsis et le type de chaque paramètre doit être compatible avec le type résultant de l'application des promotions d'argument par défaut. Si un type a une liste de types de paramètres et que l'autre type est spécifié par une définition de fonction qui contient une liste d'identificateurs (éventuellement vide), les deux doivent convenir du nombre de paramètres et le type de chaque paramètre prototype doit être compatible avec le type qui résulte de l'application des promotions d'argument par défaut 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 une fonction ou un type de tableau est considéré comme ayant le type ajusté et chaque paramètre déclaré avec un 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 ‘‘ à l'ancienne ’’, 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 brouillon de la norme C99 (PDF ) .

La règle pertinente ici se trouve au paragraphe 6.7.5.1, paragraphe 2:

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

Par conséquent, puisqu'un void*n'est pas compatible avec un 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 sortir en toute sécurité avec des pointeurs de fonction de casting 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 aucune notion de types au niveau du code machine.

Ce que vous ne pouvez certainement pas faire:

  • Cast entre les pointeurs de fonction de différentes conventions d'appel. Vous allez gâcher la pile et au mieux, planter, au pire, réussir en silence avec un énorme trou de sécurité béant. Dans la programmation Windows, vous passez souvent des pointeurs de fonction. Win32 s'attend à ce que toutes les fonctions de rappel utilisent la convention d'appel stdcall (à laquelle les macros CALLBACK, Pascal et WINAPI se développent toutes). Si vous passez un pointeur de fonction qui utilise la convention d'appel C standard (cdecl), un mauvais résultat en résultera.
  • En C++, transtypez entre des pointeurs de fonction de membre de classe et des pointeurs de fonction standard. Cela déclenche souvent les débutants en C++. Les fonctions membres de classe ont un paramètre this masqué, et si vous transtypez une fonction membre en fonction régulière, il n'y a pas d'objet this à utiliser, et là encore, beaucoup de mal en résultera.

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

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

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

Tout au long du code, il existe de nombreuses instances de conversion de type (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)

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 dans 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 êtes membre du comité des normes. Adam et Johannes méritent le crédit 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/fonctionne/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). Que les normes soient maudites, je suppose.

J'ai passé un peu de 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 risques et périls.
  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 amorcé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-ci. Je parie qu'il y a des processeurs/systèmes intégrés qui ne l'aiment pas. Cependant, pour l'informatique de bureau (et probablement mobile/tablette), cette astuce fonctionne probablement toujours.

26
kevinarpe

Le point n'est vraiment pas de savoir si vous le 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 s'il est vraiment nécessaire, auquel cas vous seriez content qu'il l'ait fait.

7
MSalters

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 "même type" juste plus laxiste pour permettre d'avoir des types différents mais toujours avoir une certaine forme de dire "ces types sont presque les mêmes". En C89, par exemple, deux structures étaient compatibles si elles étaient par ailleurs identiques mais juste leur nom était différent. C99 semble avoir changé cela. Citation du c document de justification (lecture fortement recommandée, au fait!):

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 include, car les unités de traduction sont elles-mêmes disjointes. La norme spécifie donc 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 - ouais strictement c'est un comportement indéfini, parce que 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 compilent et l'exécutent sans gémir. Mais vous pouvez faire plus propre en ayant une autre fonction prenant un void* (et l'enregistrer comme fonction de rappel) qui appellera alors votre fonction réelle.

5

Comme le code C se compile en instructions qui ne se soucient pas du tout des types de pointeurs, il est très bien d'utiliser le code que vous mentionnez. Vous rencontriez des problèmes lorsque vous exécutiez do_stuff avec votre fonction de rappel et pointiez 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.

3
che

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++) NULL est une macro définie comme ((void *)0).

Regardez 6.3.2.3 (article 1) dans C99:

Un pointeur vers void peut être converti vers ou depuis un pointeur vers n'importe quel type d'objet incomplet

0
xslogic

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, exécutent, puis pop 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é.

0
Steve Rowe