Cast di un puntatore a funzione su un altro tipo


89

Diciamo che ho una funzione che accetta un void (*)(void*)puntatore a funzione da utilizzare come callback:

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

Ora, se ho una funzione come questa:

void my_callback_function(struct my_struct* arg);

Posso farlo in sicurezza?

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

Ho esaminato questa domanda e alcuni standard C che dicono che è possibile eseguire il cast a "puntatori a funzione compatibili", ma non riesco a trovare una definizione di cosa significhi "puntatore a funzione compatibile".


1
Sono un po 'un principiante, ma cosa significa un " puntatore a funzione void ( ) (void )" ?. È un puntatore a una funzione che accetta un void * come argomento e restituisce void
Digital Gal

2
@ Myke: void (*func)(void *)significa che funcè un puntatore a una funzione con una firma del tipo come void foo(void *arg). Quindi sì, hai ragione.
mk12

Risposte:


122

Per quanto riguarda lo standard C, se si lancia un puntatore a funzione su un puntatore a funzione di un tipo diverso e poi lo si chiama, si tratta di un comportamento indefinito . Vedi Allegato J.2 (informativo):

Il comportamento non è definito nelle seguenti circostanze:

  • Un puntatore viene utilizzato per chiamare una funzione il cui tipo non è compatibile con il tipo puntato (6.3.2.3).

La sezione 6.3.2.3, paragrafo 8 recita:

Un puntatore a una funzione di un tipo può essere convertito in un puntatore a una funzione di un altro tipo e viceversa; il risultato deve essere paragonato al puntatore originale. Se un puntatore convertito viene utilizzato per chiamare una funzione il cui tipo non è compatibile con il tipo a cui punta, il comportamento è indefinito.

Quindi, in altre parole, puoi eseguire il cast di un puntatore a funzione su un diverso tipo di puntatore a funzione, eseguirne nuovamente il cast e richiamarlo e le cose funzioneranno.

La definizione di compatibile è alquanto complicata. Può essere trovato nella sezione 6.7.5.3, paragrafo 15:

Affinché due tipi di funzioni siano compatibili, entrambi devono specificare i tipi restituiti compatibili 127 .

Inoltre, gli elenchi dei tipi di parametro, se presenti entrambi, dovranno concordare nel numero dei parametri e nell'uso dei puntini di sospensione; i parametri corrispondenti devono avere tipi compatibili. Se un tipo ha un elenco di tipi di parametro e l'altro tipo è specificato da un dichiaratore di funzione che non fa parte di una definizione di funzione e che contiene un elenco di identificatori vuoto, l'elenco di parametri non deve avere un terminatore con puntini di sospensione e il tipo di ciascun parametro deve essere compatibile con il tipo che risulta dall'applicazione delle promozioni degli argomenti predefiniti. Se un tipo ha un elenco di tipi di parametri e l'altro tipo è specificato da una definizione di funzione che contiene un elenco di identificatori (possibilmente vuoto), entrambi devono concordare sul numero di parametri, e il tipo di ogni parametro del prototipo deve essere compatibile con il tipo che risulta dall'applicazione delle promozioni degli argomenti predefiniti al tipo dell'identificatore corrispondente. (Nella determinazione della compatibilità del tipo e di un tipo composto, ogni parametro dichiarato con funzione o tipo di matrice è considerato come avente il tipo corretto e ogni parametro dichiarato con tipo qualificato è considerato avente la versione non qualificata del suo tipo dichiarato.)

127) Se entrambi i tipi di funzione sono "vecchio stile", i tipi di parametro non vengono confrontati.

Le regole per determinare se due tipi sono compatibili sono descritte nella sezione 6.2.7, e non le citerò qui poiché sono piuttosto lunghe, ma puoi leggerle sulla bozza dello standard C99 (PDF) .

La regola pertinente qui è nella sezione 6.7.5.1, paragrafo 2:

Affinché due tipi di puntatori siano compatibili, entrambi devono essere qualificati in modo identico ed entrambi devono essere puntatori a tipi compatibili.

Quindi, poiché a void* non è compatibile con a struct my_struct*, un puntatore a funzione di tipo void (*)(void*)non è compatibile con un puntatore a funzione di tipo void (*)(struct my_struct*), quindi questo casting di puntatori a funzione è un comportamento tecnicamente indefinito.

In pratica, tuttavia, in alcuni casi puoi tranquillamente farla franca con il casting di puntatori a funzione. Nella convenzione di chiamata x86, gli argomenti vengono inseriti nello stack e tutti i puntatori hanno la stessa dimensione (4 byte in x86 o 8 byte in x86_64). Chiamare un puntatore a funzione si riduce a spingere gli argomenti sullo stack e fare un salto indiretto alla destinazione del puntatore a funzione, e ovviamente non c'è nozione di tipi a livello di codice macchina.

Cose che sicuramente non puoi fare:

  • Cast tra puntatori a funzione di diverse convenzioni di chiamata. Incasinerai lo stack e nel migliore dei casi, andrai in crash, nel peggiore dei casi, riuscirai silenziosamente con un enorme buco di sicurezza aperto. Nella programmazione Windows, si passano spesso i puntatori alle funzioni. Win32 si aspetta che tutte le funzioni di callback per utilizzare la stdcallconvenzione di chiamata (che le macro CALLBACK, PASCALe WINAPItutto si espandono a). Se si passa un puntatore a funzione che utilizza la convenzione di chiamata C standard ( cdecl), ne risulterà una cattiveria.
  • In C ++, eseguire il cast tra puntatori a funzione membro di classe e puntatori a funzione regolare. Questo spesso inciampa i neofiti del C ++. Le funzioni membro della classe hanno un thisparametro nascosto e se si esegue il cast di una funzione membro in una funzione normale, non ci sono thisoggetti da usare e, di nuovo, ne risulterà molta cattiveria.

Un'altra pessima idea che a volte potrebbe funzionare ma è anche un comportamento indefinito:

  • Casting tra puntatori a funzione e puntatori regolari (ad esempio casting da a void (*)(void)ad a void*). I puntatori a funzione non hanno necessariamente le stesse dimensioni dei puntatori regolari, poiché su alcune architetture potrebbero contenere informazioni contestuali extra. Questo probabilmente funzionerà bene su x86, ma ricorda che è un comportamento indefinito.

18
Non è che il punto void*è che sono compatibili con qualsiasi altro puntatore? Non dovrebbero esserci problemi a lanciare un struct my_struct*a a void*, infatti non dovresti nemmeno dover eseguire il cast, il compilatore dovrebbe semplicemente accettarlo. Ad esempio, se si passa a struct my_struct*a una funzione che accetta a void*, non è richiesto alcun casting. Cosa mi manca qui che li rende incompatibili?
brianmearns

2
Questa risposta fa riferimento a "Questo probabilmente funzionerà bene su x86 ...": Esistono piattaforme in cui NON funzionerà? Qualcuno ha esperienza quando questo ha fallito? qsort () per C sembra un bel posto per lanciare un puntatore a funzione, se possibile.
kevinarpe

4
@KCArpe: in base al grafico sotto l'intestazione "Implementazioni di puntatori funzione membro" in questo articolo , il compilatore OpenWatcom a 16 bit utilizza talvolta un tipo di puntatore a funzione più grande (4 byte) rispetto al tipo di puntatore dati (2 byte) in determinate configurazioni. Tuttavia, i sistemi conformi a POSIX devono utilizzare la stessa rappresentazione dei void*tipi di puntatori a funzione, vedere le specifiche .
Adam Rosenfield

3
Il collegamento da @adam ora fa riferimento all'edizione 2016 dello standard POSIX in cui la sezione 2.12.3 pertinente è stata rimossa. Puoi ancora trovarlo nell'edizione 2008 .
Martin Trenkmann

6
@brianmearns No, void *è solo "compatibile con" qualsiasi altro puntatore (non funzione) in modi definiti molto precisamente (che non sono correlati a ciò che lo standard C significa con la parola "compatibile" in questo caso). C consente a void *a di essere più grande o più piccolo di a struct my_struct *, o di avere i bit in ordine diverso o negati o altro. Quindi void f(void *)e void f(struct my_struct *)può essere incompatibile ABI . C convertirà i puntatori stessi per te se necessario, ma non lo farà e talvolta non potrebbe convertire una funzione a cui punta per accettare un tipo di argomento possibilmente diverso.
mtraceur

32

Di recente ho chiesto informazioni su questo identico problema riguardante alcuni codici in GLib. (GLib è una libreria principale per il progetto GNOME ed è scritta in C.) Mi è stato detto che l'intero framework di slots'n'signals dipende da questo.

In tutto il codice sono presenti numerose istanze di casting dal 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)

È comune eseguire il chain-thru con chiamate come questa:

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

Guarda tu stesso qui in g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Le risposte di cui sopra sono dettagliate e probabilmente corrette - se fai parte del comitato per gli standard. Adam e Johannes meritano credito per le loro risposte ben studiate. Tuttavia, in natura, scoprirai che questo codice funziona perfettamente. Controverso? Sì. Considera questo: GLib compila / funziona / testa su un gran numero di piattaforme (Linux / Solaris / Windows / OS X) con un'ampia varietà di compilatori / linker / caricatori del kernel (GCC / CLang / MSVC). Al diavolo gli standard, immagino.

Ho passato un po 'di tempo a pensare a queste risposte. Ecco la mia conclusione:

  1. Se stai scrivendo una libreria di callback, potrebbe andare bene. Avvertimento: usalo a tuo rischio.
  2. Altrimenti, non farlo.

Pensando più a fondo dopo aver scritto questa risposta, non sarei sorpreso se il codice per i compilatori C usasse lo stesso trucco. E poiché (la maggior parte / tutti?) I moderni compilatori C sono bootstrap, ciò implicherebbe che il trucco è sicuro.

Una domanda più importante da ricercare: qualcuno può trovare una piattaforma / compilatore / linker / loader in cui questo trucco non funziona? I principali punti brownie per quello. Scommetto che ci sono alcuni processori / sistemi incorporati a cui non piace. Tuttavia, per il computer desktop (e probabilmente per dispositivi mobili / tablet), questo trucco probabilmente funziona ancora.


10
Un posto dove sicuramente non funziona è il compilatore Emscripten LLVM to Javascript. Vedi github.com/kripken/emscripten/wiki/Asm-pointer-casts per i dettagli.
Ben Lings

2
Riferimento aggiornato sull'Emscripten .
ysdx

4
Il collegamento @BenLings pubblicato si interromperà nel prossimo futuro. Si è ufficialmente spostato su kripken.github.io/emscripten-site/docs/porting/guidelines/…
Alex Reinking

9

Il punto non è davvero se puoi. La soluzione banale è

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 buon compilatore genererà codice per my_callback_helper solo se è davvero necessario, nel qual caso saresti contento che lo facesse.


Il problema è che questa non è una soluzione generale. Deve essere fatto caso per caso con la conoscenza della funzione. Se hai già una funzione del tipo sbagliato, sei bloccato.
BeeOnRope

Tutti i compilatori con cui l'ho testato genereranno codice my_callback_helper, a meno che non sia sempre inline. Questo non è assolutamente necessario, poiché l'unica cosa che tende a fare è jmp my_callback_function. Il compilatore probabilmente vuole assicurarsi che gli indirizzi per le funzioni siano diversi, ma sfortunatamente lo fa anche quando la funzione è contrassegnata con C99 inline(cioè "non importa l'indirizzo").
yyny

Non sono sicuro che sia corretto. Un altro commento da un'altra risposta sopra (di @mtraceur) dice che a void *può essere anche di dimensioni diverse da a struct *(penso che sia sbagliato, perché altrimenti mallocsarebbe rotto, ma quel commento ha 5 voti positivi, quindi gli do un po 'di merito. Se @mtraceur ha ragione, la soluzione che hai scritto non sarebbe corretta.
termina il

@cesss: non importa affatto se la dimensione è diversa. La conversione da e verso void*deve ancora funzionare. In breve, void*può avere più bit, ma se lanci a struct*su void*quei bit extra possono essere zeri e il cast può semplicemente scartare di nuovo quegli zeri.
MSalters

@MSalters: davvero non sapevo che un void *potrebbe (in teoria) essere così diverso da un file struct *. Sto implementando un vtable in C e sto usando un thispuntatore ish C ++ come primo argomento per le funzioni virtuali. Ovviamente, thisdeve essere un puntatore alla struttura "corrente" (derivata). Quindi, le funzioni virtuali necessitano di prototipi diversi a seconda della struttura in cui sono implementate. Pensavo che l'uso di un void *thisargomento avrebbe risolto tutto ma ora ho imparato che è un comportamento indefinito ...
termina il

6

Hai un tipo di funzione compatibile se il tipo restituito ei tipi di parametro sono compatibili - in pratica (è più complicato in realtà :)). La compatibilità è la stessa di "stesso tipo" solo più lassista per consentire di avere tipi diversi ma avere ancora qualche forma di dire "questi tipi sono quasi gli stessi". In C89, ad esempio, due strutture erano compatibili se per il resto erano identiche ma solo il loro nome era diverso. C99 sembra aver cambiato la situazione. Citando dal documento logico (lettura altamente raccomandata, btw!):

Le dichiarazioni di tipo di struttura, unione o enumerazione in due diverse unità di traduzione non dichiarano formalmente lo stesso tipo, anche se il testo di queste dichiarazioni proviene dallo stesso file di inclusione, poiché le unità di traduzione sono esse stesse disgiunte. Lo Standard specifica quindi regole di compatibilità aggiuntive per tali tipi, in modo che se due di tali dichiarazioni sono sufficientemente simili sono compatibili.

Detto questo, si rigorosamente questo è un comportamento indefinito, perché la tua funzione do_stuff o qualcun altro chiamerà la tua funzione con un puntatore a funzione che ha void*come parametro, ma la tua funzione ha un parametro incompatibile. Tuttavia, mi aspetto che tutti i compilatori lo compilino ed eseguano senza lamentarsi. Ma puoi fare più pulizia avendo un'altra funzione che prende un void*(e lo registra come funzione di callback) che chiamerà semplicemente la tua funzione effettiva.


4

Poiché il codice C si compila in istruzioni che non si preoccupano affatto dei tipi di puntatore, è abbastanza buono usare il codice che hai menzionato. Avresti avuto problemi quando avresti eseguito do_stuff con la tua funzione di callback e puntatore a qualcos'altro, quindi la struttura my_struct come argomento.

Spero di poterlo rendere più chiaro mostrando cosa non funzionerebbe:

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

Fondamentalmente, puoi lanciare puntatori a qualsiasi cosa tu voglia, purché i dati continuino ad avere senso in fase di esecuzione.


0

Se pensi al modo in cui funzionano le chiamate di funzione in C / C ++, inseriscono determinati elementi nello stack, salta alla nuova posizione del codice, esegue, quindi inserisce lo stack al ritorno. Se i tuoi puntatori a funzione descrivono funzioni con lo stesso tipo di ritorno e lo stesso numero / dimensione di argomenti, dovresti stare bene.

Quindi, penso che dovresti essere in grado di farlo in sicurezza.


2
sei al sicuro solo finché i structpuntatori e i voidpuntatori hanno rappresentazioni bit compatibili; non è garantito che sia così
Christoph

1
I compilatori possono anche passare argomenti nei registri. E non è insolito utilizzare registri diversi per float, int o puntatori.
MSalters

0

I puntatori vuoti sono compatibili con altri tipi di puntatori. È la spina dorsale di come funzionano malloc e le funzioni mem ( memcpy, memcmp). In genere, in C (piuttosto che C ++) NULLè una macro definita come ((void *)0).

Guarda 6.3.2.3 (elemento 1) in C99:

Un puntatore a void può essere convertito in o da un puntatore a qualsiasi tipo di oggetto o incompleto


Ciò è in contraddizione con la risposta di Adam Rosenfield , vedere l'ultimo paragrafo e commenti
utente

1
Questa risposta è chiaramente sbagliata. Qualsiasi puntatore è convertibile in un puntatore void, ad eccezione dei puntatori a funzione.
marton78
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.