Conversión de un puntero de función a otro tipo

89

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

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 examinado esta pregunta y he examinado algunos estándares de C que dicen que puede convertir a 'punteros de función compatibles', pero no puedo encontrar una definición de lo que significa 'puntero de función compatible'.

Mike Weller
fuente
1
Soy un poco novato, pero ¿qué significa un " puntero de función void ( ) (void )"? ¿Es un puntero a una función que acepta un vacío * como argumento y devuelve vacío
Digital Gal
2
@Myke: void (*func)(void *)significa que funces un puntero a una función con una firma de tipo como void foo(void *arg). Entonces sí, tienes razón.
mk12

Respuestas:

122

En lo que respecta al estándar C, si lanza un puntero de función a un puntero de función de un tipo diferente y luego lo llama, es un comportamiento indefinido . Ver Anexo J.2 (informativo):

El comportamiento no está definido en las siguientes circunstancias:

  • Un puntero se usa 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 puede convertirse en un puntero a una función de otro tipo y viceversa; el resultado se comparará igual al puntero original. Si se utiliza un puntero convertido 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 a un tipo de puntero de función diferente, devolverlo de nuevo y llamarlo, y todo funcionará.

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 especificarán tipos de retorno compatibles 127 .

Además, las listas de tipos de parámetros, si ambos 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 deberán tener tipos compatibles. Si un tipo tiene una lista de tipos de parámetros y el otro tipo está especificado por un declarador de función que no forma 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 deberá ser compatible con el tipo que resulta de la aplicación de las promociones de argumento por defecto. Si un tipo tiene una lista de tipos de parámetros y el otro tipo se especifica mediante una definición de función que contiene una lista de identificadores (posiblemente vacía), ambos estarán de acuerdo en el número de parámetros, y el tipo de cada parámetro prototipo será compatible con el tipo que resulte de la aplicación de las promociones de argumentos por defecto al tipo del identificador correspondiente. (En la determinación de la 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 las citaré aquí porque son bastante extensas, pero puede leerlas 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 estar calificados de manera idéntica y ambos deben ser punteros a tipos compatibles.

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

Sin embargo, en la práctica, en algunos casos puede salirse con la suya sin problemas con los punteros de función de lanzamiento. En la convención de llamadas x86, los argumentos se insertan en la pila 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 empujar los argumentos en la pila y hacer un salto indirecto al destino del puntero de función, y obviamente no hay noción de tipos a nivel de código de máquina.

Cosas que definitivamente no puedes hacer:

  • Conversión entre punteros de función de diferentes convenciones de llamada. Arruinará la pila y, en el mejor de los casos, se estrellará, en el peor, tendrá éxito en silencio con un enorme agujero de seguridad. En la programación de Windows, a menudo se pasan punteros de función. Win32 espera que todas las funciones de devolución de llamada para utilizar la stdcallconvención de llamada (que las macros CALLBACK, PASCALy WINAPItodo se expanden a). Si pasa un puntero de función que usa la convención de llamada estándar de C ( cdecl), resultará mal.
  • En C ++, conversión entre punteros de función de miembro de clase y punteros de función regulares. Esto a menudo hace tropezar a los novatos de C ++. Las funciones miembro de la clase tienen un thisparámetro oculto , y si convierte una función miembro a una función normal, no hay ningún thisobjeto para usar y, nuevamente, resultará en mucha maldad.

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

  • Conversión entre punteros de función y punteros regulares (por ejemplo, conversión de a void (*)(void)a a void*). Los punteros de función no tienen necesariamente el mismo tamaño que los punteros normales, ya que en algunas arquitecturas pueden contener información contextual adicional. Esto probablemente funcionará bien en x86, pero recuerde que es un comportamiento indefinido.
Adam Rosenfield
fuente
18
¿No se void*trata de que son compatibles con cualquier otro puntero? No debería haber ningún problema para convertir a struct my_struct*a a void*, de hecho, ni siquiera debería tener que emitir, el compilador debería aceptarlo. Por ejemplo, si pasa a struct my_struct*a una función que toma a void*, no se requiere conversión. ¿Qué me estoy perdiendo aquí que los hace incompatibles?
brianmearns
2
Esta respuesta hace referencia a "Esto probablemente funcionará bien en x86 ...": ¿Hay alguna plataforma en la que esto NO funcione? ¿Alguien tiene experiencia cuando esto falló? qsort () para C parece un buen lugar para lanzar un puntero de función si es posible.
kevinarpe
4
@KCArpe: De acuerdo con la tabla bajo el título "Implementaciones de punteros de función miembro" en este artículo , el compilador OpenWatcom de 16 bits a veces usa un tipo de puntero de función más grande (4 bytes) que el tipo de puntero de datos (2 bytes) en ciertas configuraciones. Sin embargo, los sistemas que cumplen con POSIX deben usar la misma representación para los void*tipos de puntero de función, consulte las especificaciones .
Adam Rosenfield
3
El enlace de @adam ahora se refiere a la edición 2016 del estándar POSIX donde se ha eliminado la sección 2.12.3 correspondiente. Aún puede encontrarlo en la edición de 2008 .
Martin Trenkmann
6
@brianmearns No, void *solo es "compatible con" cualquier otro puntero (sin función) de formas definidas con mucha precisión (que no están relacionadas con lo que el estándar C significa con la palabra "compatible" en este caso). C permite que void *a sea más grande o más pequeño que a struct my_struct *, o tener los bits en un orden diferente o negados o lo que sea. Entonces, void f(void *)y void f(struct my_struct *)puede ser incompatible con ABI . C convertirá los punteros por usted si es necesario, pero no lo hará y, a veces, no podría convertir una función apuntada para que tome un tipo de argumento posiblemente diferente.
mtraceur
32

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 slots'n'signals depende de ello.

A lo largo del código, existen numerosos casos de conversión del tipo (1) al (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 encadenar 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;
}

Véalo 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 forma parte del comité de normas. Adam y Johannes merecen crédito por sus respuestas bien investigadas. Sin embargo, en la naturaleza, encontrará que este código funciona bien. ¿Polémico? Si. Considere esto: GLib compila / trabaja / prueba en una gran cantidad de plataformas (Linux / Solaris / Windows / OS X) con una amplia variedad de compiladores / enlazadores / cargadores de kernel (GCC / CLang / MSVC). Al diablo con los estándares, supongo.

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

  1. Si está escribiendo una biblioteca de devolución de llamada, 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 compiladores de C usa este mismo truco. Y dado que (¿la mayoría / todos?) Los compiladores de C modernos son bootstrap, esto implicaría que el truco es seguro.

Una pregunta más importante para investigar: ¿Puede alguien encontrar una plataforma / compilador / enlazador / cargador donde este truco funciona? no funcione? Grandes puntos de brownie para ese. Apuesto a que hay algunos procesadores / sistemas integrados a los que no les gusta. Sin embargo, para computadoras de escritorio (y probablemente móviles / tabletas), este truco probablemente aún funcione.

kevinarpe
fuente
10
Un lugar donde definitivamente no funciona es el compilador Emscripten LLVM to Javascript. Consulte github.com/kripken/emscripten/wiki/Asm-pointer-casts para obtener más detalles.
Ben Lings
2
Referencia actualizada sobre Emscripten .
ysdx
4
El enlace que @BenLings publicó se romperá en un futuro próximo. Se ha trasladado oficialmente a kripken.github.io/emscripten-site/docs/porting/guidelines/…
Alex Reinking
9

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 compilador solo generará código para my_callback_helper si es realmente necesario, en cuyo caso te alegrarás de que así sea.

MSalters
fuente
El problema es que esta no es una solución general. Debe hacerse caso por caso con conocimiento de la función. Si ya tiene una función del tipo incorrecto, está atascado.
BeeOnRope
Todos los compiladores con los que probé esto generarán código my_callback_helper, a menos que siempre esté en línea. Definitivamente, esto no es necesario, ya que lo único que suele hacer es jmp my_callback_function. El compilador probablemente quiera asegurarse de que las direcciones de las funciones sean diferentes, pero desafortunadamente lo hace incluso cuando la función está marcada con C99 inline(es decir, "no me importa la dirección").
yyny
No estoy seguro de que esto sea correcto. Otro comentario de otra respuesta anterior (por @mtraceur) dice que a void *puede ser incluso de diferente tamaño que a struct *(creo que eso es incorrecto, porque de lo contrario mallocestaría roto, pero ese comentario tiene 5 votos a favor, así que le doy algo de crédito. Si @mtraceur es correcto, la solución que escribió no sería correcto.
cesss
@cesss: No importa en absoluto si el tamaño es diferente. La conversión hacia y desde void*todavía tiene que funcionar. En resumen, void*puede tener más bits, pero si lanza struct*a void*esos bits adicionales pueden ser ceros y la conversión puede simplemente descartar esos ceros nuevamente.
MSalters
@MSalters: Realmente no sabía que una void *podría (en teoría) ser tan diferente de una struct *. Estoy implementando una vtable en C, y estoy usando un thispuntero C ++ - ish como el primer argumento para las funciones virtuales. Obviamente, thisdebe ser un puntero a la estructura "actual" (derivada). Entonces, las funciones virtuales necesitan diferentes prototipos dependiendo de la estructura en la que se implementan. Pensé que usar un void *thisargumento arreglaría todo, pero ahora aprendí que es un comportamiento indefinido ...
cesss
6

Tiene un tipo de función compatible si el tipo de retorno y los tipos de parámetros son compatibles, básicamente (es más complicado en realidad :)). La compatibilidad es la misma que "mismo tipo", pero más laxa para permitir tener diferentes tipos, pero aún así tener alguna forma de decir "estos tipos son casi iguales". En C89, por ejemplo, dos estructuras eran compatibles si eran idénticas pero su nombre era diferente. C99 parece haber cambiado eso. Citando del documento de fundamento c (lectura muy recomendable, por cierto!):

Las declaraciones de tipo de 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 disjuntas. Por tanto, la Norma especifica reglas de compatibilidad adicionales para tales tipos, de modo que si dos de tales declaraciones son suficientemente similares, sean compatibles.

Dicho esto, sí, estrictamente, este es un comportamiento indefinido, porque su función do_stuff o alguien más llamará a su función con un puntero de función que tenga void*como parámetro, pero su función tiene un parámetro incompatible. Sin embargo, espero que todos los compiladores lo compilen y lo ejecuten sin quejarse. Pero puede hacerlo más limpio si tiene otra función que toma un void*(y lo registra como función de devolución de llamada) que simplemente llamará a su función real en ese momento.

Johannes Schaub - litb
fuente
4

Como el código C se compila con instrucciones que no se preocupan en absoluto por los tipos de punteros, está bastante bien usar el código que menciona. Tendría problemas cuando ejecutara do_stuff con su función de devolución de llamada y el puntero a otra cosa que no sea la estructura my_struct como argumento.

Espero poder hacerlo más claro 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, puede lanzar punteros a lo que quiera, siempre que los datos sigan teniendo sentido en tiempo de ejecución.

che
fuente
0

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

Por lo tanto, creo que debería poder hacerlo de forma segura.

Steve Rowe
fuente
2
sólo está a salvo siempre que struct-pointers y void-pointers tengan representaciones de bits compatibles; no se garantiza que ese sea el caso
Christoph
1
Los compiladores también pueden pasar argumentos en registros. Y no es raro usar diferentes registros para flotadores, ints o punteros.
MSalters
0

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

Mire 6.3.2.3 (elemento 1) en C99:

Un puntero a vacío se puede convertir ao desde un puntero a cualquier tipo de objeto o incompleto

xslogic
fuente
Esto contradice la respuesta de Adam Rosenfield , vea el último párrafo y comentarios
usuario
1
Esta respuesta es claramente incorrecta. Cualquier puntero se puede convertir en un puntero vacío, excepto los punteros de función.
marton78