Lanzar un puntero a otro tipo de función

Digamos que tengo una función que acepta un puntero de función void (*)(void*) para usar como callback:

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

Ahora, si tengo una función como esta:

 void my_callback_function(struct my_struct* arg); 

¿Puedo hacer esto de forma segura?

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

He visto esta pregunta y he analizado algunos estándares de C que dicen que puedes convertir a ‘punteros de función compatibles’, pero no puedo encontrar una definición de lo que significa ‘puntero de función compatible’.

En lo que se refiere al estándar C, si usted transfiere un puntero a un puntero de función de un tipo diferente y luego lo llama, se trata de un comportamiento indefinido . Ver el Anexo J.2 (informativo):

El comportamiento no está definido en las siguientes circunstancias:

  • Un puntero se utiliza para llamar a una función cuyo tipo no es compatible con el tipo apuntado (6.3.2.3).

La Sección 6.3.2.3, párrafo 8 dice:

Un puntero a una función de un tipo se puede convertir en un puntero a una función de otro tipo y viceversa; el resultado se comparará igual al puntero original. Si un puntero convertido se usa para llamar a una función cuyo tipo no es compatible con el tipo apuntado, el comportamiento no está definido.

En otras palabras, puede convertir un puntero de función en un tipo de puntero de función diferente, devolverlo nuevamente y llamarlo, y las cosas funcionarán.

La definición de compatible es algo complicada. Se puede encontrar en la sección 6.7.5.3, párrafo 15:

Para que dos tipos de funciones sean compatibles, ambos deberán especificar tipos de retorno compatibles 127 .

Además, las listas de tipos de parámetros, si ambas están presentes, coincidirán en el número de parámetros y en el uso del terminador de puntos suspensivos; los parámetros correspondientes deben tener tipos compatibles. Si un tipo tiene una lista de tipos de parámetros y el otro tipo está especificado por un declarador de funciones que no es parte de una definición de función y que contiene una lista de identificadores vacía, la lista de parámetros no tendrá un terminador de puntos suspensivos y el tipo de cada parámetro ser compatible con el tipo que resulta de la aplicación de las promociones de argumento predeterminadas. Si un tipo tiene una lista de tipos de parámetros y el otro tipo está especificado por una definición de función que contiene una lista de identificadores (posiblemente vacía), ambos acordarán en el número de parámetros, y el tipo de cada parámetro prototipo será compatible con el tipo que resulta de la aplicación de las promociones de argumento predeterminadas al tipo del identificador correspondiente. (En la determinación de compatibilidad de tipos y de un tipo compuesto, cada parámetro declarado con función o tipo de matriz se considera que tiene el tipo ajustado y cada parámetro declarado con tipo calificado se considera que tiene la versión no calificada de su tipo declarado).

127) Si ambos tipos de funciones son ” estilo antiguo ”, los tipos de parámetros no se comparan.

Las reglas para determinar si dos tipos son compatibles se describen en la sección 6.2.7, y no los citaré aquí porque son bastante largos, pero puede leerlos en el borrador del estándar C99 (PDF) .

La regla relevante aquí está en la sección 6.7.5.1, párrafo 2:

Para que dos tipos de punteros sean compatibles, ambos deben ser idénticamente calificados y ambos deben ser punteros a tipos compatibles.

Por lo tanto, dado que un void* no es compatible con una struct my_struct* , un puntero a función de tipo void (*)(void*) no es compatible con un puntero de función de tipo void (*)(struct my_struct*) , por lo que este vaciado de punteros de función es un comportamiento técnicamente indefinido.

En la práctica, sin embargo, puede salirse con la seguridad con los punteros de función de fundición en algunos casos. En la convención de llamadas x86, los argumentos se insertan en la stack y todos los punteros tienen el mismo tamaño (4 bytes en x86 u 8 bytes en x86_64). Llamar a un puntero de función se reduce a presionar los argumentos en la stack y hacer un salto indirecto al objective del puntero de función, y obviamente no hay ninguna noción de tipos en el nivel de código de máquina.

Cosas que definitivamente no puedes hacer:

  • Emitir punteros de función de diferentes convenciones de llamada. Destruirá la stack y, en el mejor de los casos, chocará, en el peor de los casos, triunfará silenciosamente con un enorme agujero de seguridad. En la progtwigción de Windows, a menudo se pasan punteros a las funciones. Win32 espera que todas las funciones de callback utilicen la convención de llamadas stdcall (a la que se CALLBACK las macros CALLBACK , PASCAL y WINAPI ). Si pasa un puntero de función que usa la convención de llamada C estándar ( cdecl ), se generará maldad.
  • En C ++, envía los punteros a las funciones de los miembros de la clase y los punteros de funciones regulares. Esto a menudo hace tropezar a los novatos en C ++. Las funciones de miembros de la clase tienen un parámetro oculto, y si asigna una función de miembro a una función normal, no hay que usar this objeto, y de nuevo, se generará mucha maldad.

Otra mala idea que a veces podría funcionar pero también es un comportamiento indefinido:

  • Casting entre punteros de función y punteros regulares (por ejemplo, lanzando un void (*)(void) a un void* ). Los punteros de función no son necesariamente del mismo tamaño que los punteros regulares, ya que en algunas architectures pueden contener información contextual adicional. Esto probablemente funcionará bien en x86, pero recuerda que es un comportamiento indefinido.

Pregunté sobre este mismo problema con respecto a algún código en GLib recientemente. (GLib es una biblioteca central para el proyecto GNOME y está escrita en C.) Me dijeron que todo el marco de señales de los tragamonedas depende de ello.

A lo largo del código, hay numerosas instancias de conversión del tipo (1) a (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 común realizar una conexión de cadena con llamadas como esta:

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

g_array_sort() usted mismo aquí en g_array_sort() : http://git.gnome.org/browse/glib/tree/glib/garray.c

Las respuestas anteriores son detalladas y probablemente correctas, si participa en el comité de normas. Adam y Johannes merecen crédito por sus respuestas bien documentadas. Sin embargo, en la naturaleza, encontrarás que este código funciona bien. ¿Polémico? Sí. Considere esto: GLib comstack / trabaja / prueba en un gran número de plataformas (Linux / Solaris / Windows / OS X) con una amplia variedad de comstackdores / enlazadores / cargadores de kernel (GCC / CLang / MSVC). Los estándares están malditos, supongo.

Pasé un tiempo pensando en estas respuestas. Aquí está mi conclusión:

  1. Si está escribiendo una biblioteca de callback, esto podría estar bien. Caveat Emptor: utilícelo bajo su propio riesgo.
  2. De lo contrario, no lo hagas.

Pensando más profundamente después de escribir esta respuesta, no me sorprendería si el código para los comstackdores de C usa este mismo truco. Y dado que (¿la mayoría / todos?) Los comstackdores de C modernos son bootstrap, esto implicaría que el truco es seguro.

Una pregunta más importante para investigar: ¿Alguien puede encontrar una plataforma / comstackdor / enlazador / cargador donde este truco no funciona? Puntos importantes de brownie para eso. Apuesto a que hay algunos procesadores / sistemas integrados a los que no les gusta. Sin embargo, para computadoras de escritorio (y probablemente para dispositivos móviles / tabletas), este truco probablemente todavía funcione.

El punto realmente no es si puedes. La solución trivial es

 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 buen comstackdor solo generará código para my_callback_helper si es realmente necesario, en cuyo caso te alegraría que lo hiciera.

Tiene un tipo de función compatible si el tipo de devolución y los tipos de parámetros son compatibles, básicamente (es más complicado en realidad :)). La compatibilidad es igual que “del mismo tipo”, es más laxa para permitir tener diferentes tipos, pero aún así se puede decir que “estos tipos son casi iguales”. En C89, por ejemplo, dos estructuras eran compatibles si fueran idénticas pero su nombre era diferente. C99 parece haber cambiado eso. Citando del documento de justificación (¡lectura muy recomendada, por cierto!):

Las declaraciones de tipo estructura, unión o enumeración en dos unidades de traducción diferentes no declaran formalmente el mismo tipo, incluso si el texto de estas declaraciones proviene del mismo archivo de inclusión, ya que las unidades de traducción son en sí mismas disjuntas. El estándar especifica así las reglas de compatibilidad adicionales para tales tipos, de modo que si dos de tales declaraciones son suficientemente similares, son compatibles.

Dicho eso, sí, estrictamente, este es un comportamiento indefinido, porque su función do_stuff u otra persona llamará a su función con un puntero de función que tenga el parámetro void* como parámetro, pero su función tiene un parámetro incompatible. Sin embargo, espero que todos los comstackdores compilen y ejecuten sin quejarse. Pero puede hacer más limpio teniendo otra función void* (y registrando eso como función de callback) que luego llamará a su función real.

A medida que el código C comstack a las instrucciones que no se preocupan en absoluto por los tipos de punteros, es bastante bueno utilizar el código que menciona. Te topabas con problemas cuando ejecutabas do_stuff con tu función de callback y apuntabas a otra cosa, luego mi estructura de estructura como argumento.

Espero poder aclararlo mostrando lo que no funcionaría:

 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 

o…

 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 

Básicamente, puedes lanzar punteros a lo que quieras, siempre y cuando los datos continúen teniendo sentido en el tiempo de ejecución.

Si piensas en la forma en que funcionan las llamadas de función en C / C ++, empujan ciertos elementos en la stack, saltan a la nueva ubicación del código, se ejecutan y luego sacan la stack al regresar. Si los punteros de función describen funciones con el mismo tipo de devolución y el mismo número / tamaño de argumentos, debería estar bien.

Por lo tanto, creo que deberías ser capaz de hacerlo de manera segura.

Los punteros vacíos son compatibles con otros tipos de puntero. Es la columna vertebral de cómo funcionan las funciones malloc y mem ( memcpy , memcmp ). Normalmente, en C (en lugar de C ++) NULL es una macro definida como ((void *)0) .

Mira 6.3.2.3 (Artículo 1) en C99:

Un puntero a void se puede convertir desde o hacia un puntero a cualquier tipo incompleto o de objeto