113 votes

Transformation d'un pointeur de fonction en un autre type

Disons que j'ai une fonction qui accepte une void (*)(void*) pointeur de fonction pour l'utilisation en tant que callback :

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 J'ai consulté certaines normes C qui stipulent que l'on peut effectuer des castings vers des "pointeurs de fonction compatibles", mais je ne trouve pas de définition de ce que signifie "pointeur de fonction compatible".

2 votes

Je suis un peu novice, mais qu'est-ce qu'un "void ( )(void ) pointeur de fonction" signifie ? Est-ce un pointeur vers une fonction qui accepte un void* en tant qu'argument et renvoie void

2 votes

@Myke : void (*func)(void *) signifie que func est un pointeur vers une fonction avec une signature de type telle que void foo(void *arg) . Alors oui, vous avez raison.

140voto

Adam Rosenfield Points 176408

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

Le comportement est indé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). 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. type et inversement ; le résultat doit être égal au pointeur original. Si un pointeur converti est utilisé pour appeler une fonction dont le type n'est pas compatible avec le type pointé, le comportement est indéfini.

En d'autres termes, vous pouvez transférer un pointeur de fonction vers un autre type de pointeur de fonction, le retransférer et l'appeler, et tout fonctionnera.

La définition de compatible est quelque peu compliqué. Elle se trouve à 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 .

En outre, les listes de types de paramètres, si elles sont toutes deux présentes, doivent concorder quant au nombre de paramètres et à l'utilisation de ces paramètres. paramètres et dans l'utilisation de la terminaison des points de suspension ; les paramètres correspondants doivent avoir des types types compatibles. Si un type possède une liste de types de paramètres et que 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, ce type de déclaration n'est pas compatible avec le type de paramètre. vide, la liste des paramètres ne doit pas comporter de point de suspension et le type de chaque paramètre doit être compatible avec le type de la fonction. paramètre doit être compatible avec le type résultant de l'application des promotions d'arguments par défaut. promotions d'arguments par défaut. Si un type possède 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 s'accorder sur le nombre de paramètres, et le type de chaque paramètre prototype doit être compatible avec le type résultant de l'application des promotions d'arguments par défaut. compatible avec le type qui résulte de l'application de l'argument par défaut au type de l'identificateur correspondant. (Pour déterminer la compatibilité des types et d'un type composite, chaque paramètre déclaré avec un type de fonction ou de tableau est considéré comme ayant le type ajusté. 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 type "ancien", les types de paramètres ne sont pas comparés.

Les règles permettant de 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 site de l projet de la norme C99 (PDF) .

La règle pertinente ici se trouve à la section 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, puisque a 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*) Ce casting de pointeurs de fonction est donc techniquement un comportement non défini.

En pratique, cependant, vous pouvez sans risque vous passer du casting des pointeurs de fonction 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). L'appel d'un pointeur de fonction se résume à pousser les arguments sur la pile et à effectuer un saut indirect vers la cible du pointeur de fonction, et il n'y a évidemment aucune notion de type au niveau du code machine.

Les choses que vous avez définitivement ne peut pas faire :

  • Cast entre les pointeurs de fonction de différentes conventions d'appel. Vous allez perturber la pile et, au mieux, vous planter, au pire, réussir en silence avec une énorme faille de sécurité béante. Dans la programmation Windows, vous faites souvent circuler des pointeurs de fonction. Win32 s'attend à ce que toutes les fonctions de rappel utilisent l'attribut stdcall convention d'appel (que les macros CALLBACK , PASCAL y WINAPI se développent toutes). Si vous passez un pointeur de fonction qui utilise la convention d'appel standard du C ( cdecl ), il en résultera de la méchanceté.
  • En C++, il faut faire la distinction entre les pointeurs de fonctions de membres de classe et les pointeurs de fonctions ordinaires. Cela fait souvent trébucher les débutants en C++. Les fonctions membres d'une classe ont une fonction cachée this et si vous transformez une fonction membre en une fonction normale, il n'y a pas de paramètre this objet à utiliser, et encore une fois, beaucoup de mal en résultera.

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

  • Le transfert entre les pointeurs de fonction et les pointeurs normaux (par exemple, le transfert d'un fichier void (*)(void) à un void* ). Les pointeurs de fonction n'ont pas nécessairement la même taille que les pointeurs ordinaires, 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.

0 votes

Comme l'a souligné l'auteur de la question, cela n'interdit qu'un "type incompatible". Il semble qu'un type de fonction prenant un void* devrait être compatible avec un type de fonction prenant tout autre type de pointeur en C, mais la question est de savoir ce que dit la norme.

1 votes

@adam, je vois que tu as aussi fait tes recherches :) chuck, void* et son type struct* ne sont pas compatibles. donc c'est un comportement indéfini de l'appeler. mais de toute façon, un comportement indéfini n'est pas toujours si mauvais. si le compilateur le fait bien, pourquoi s'en inquiéter. mais dans ce cas, il existe une solution propre.

23 votes

N'est-ce pas le but de void* est qu'ils sont compatibles avec tout autre pointeur ? Il ne devrait y avoir aucun problème à couler un struct my_struct* à un void* En fait, vous ne devriez même pas avoir à effectuer de cast, le compilateur devrait simplement l'accepter. Par exemple, si vous passez un struct my_struct* à une fonction qui prend un void* pas de moulage nécessaire. Qu'est-ce qui me manque ici et qui rend ces produits incompatibles ?

38voto

kevinarpe Points 2902

J'ai récemment posé la même question à propos d'un code dans GLib. (GLib est une bibliothèque centrale du projet GNOME et est écrite en C.) On m'a dit que l'ensemble du cadre de slots'n'signals en dépendait.

Dans l'ensemble du code, il y a de nombreux cas de passage du type (1) au type (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 d'enchaîner les appels de ce type :

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 faites partie 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/travaille/teste sur un grand nombre de plateformes (Linux/Solaris/Windows/OS X) avec une grande variété de compilateurs/lienseurs/chargeurs de noyau (GCC/CLang/MSVC). Au diable les standards, je suppose.

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

  1. Si vous écrivez une bibliothèque de callbacks, cela peut être acceptable. Caveat emptor -- utilisez-le à 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 que le code des compilateurs C utilise cette même astuce. Et comme (la plupart/toutes ?) 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, un compilateur, un éditeur de liens ou un chargeur où cette astuce fait no travail ? De grands points de fidélité pour celui-là. Je parie qu'il y a des processeurs/systèmes intégrés qui n'aiment pas ça. Cependant, pour l'informatique de bureau (et probablement pour les mobiles/tablettes), cette astuce fonctionne probablement toujours.

12 votes

Le compilateur LLVM vers Javascript d'Emscripten est un endroit où cela ne fonctionne absolument pas. Voir github.com/kripken/emscripten/wiki/Asm-pointer-casts pour les détails.

2 votes

Référence actualisée sur le Emscripten .

4 votes

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

12voto

MSalters Points 74024

La question n'est pas vraiment de savoir si vous pouvez le faire. 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 serez content qu'il le fasse.

0 votes

Le problème est que ce n'est pas une solution générale. Il faut le faire au cas par cas en connaissant la fonction. Si vous avez déjà une fonction du mauvais type, vous êtes coincé.

1 votes

Tous les compilateurs avec lesquels j'ai testé ceci génèrent du code pour my_callback_helper à moins qu'il ne soit toujours inlined. Ceci n'est absolument pas nécessaire, car la seule chose que cela tend à 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 C99. inline (c'est-à-dire "ne pas se soucier de l'adresse").

1 votes

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

6voto

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 chose que le "même type", mais elle est plus souple pour permettre d'avoir des types différents, tout en ayant une forme d'expression "ces types sont presque les mêmes". En C89, par exemple, deux structs étaient compatibles s'ils étaient autrement identiques mais que leur nom était différent. C99 semble avoir changé cela. En citant le c document de justification (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 donc des règles de compatibilité supplémentaires pour ces types, de sorte que si deux déclarations de ce type sont suffisamment similaires, elles sont compatibles.

Cela dit, il s'agit bien d'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 se plaindre. Mais vous pouvez faire plus propre en ayant une autre fonction prenant un paramètre void* (et l'enregistrer comme fonction de rappel) qui appellera simplement votre fonction actuelle.

3voto

che Points 6899

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

J'espère pouvoir rendre les choses plus claires 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

En fait, vous pouvez utiliser les pointeurs comme bon vous semble, tant que les données continuent d'avoir un sens au moment de l'exécution.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X