Transmitindo um ponteiro de função para outro tipo


90

Digamos que eu tenha uma função que aceita um void (*)(void*)ponteiro de função para uso como retorno de chamada:

Agora, se eu tiver uma função como esta:

Posso fazer isso com segurança?

Eu olhei para esta questão e olhei para alguns padrões C que dizem que você pode converter para 'ponteiros de função compatíveis', mas não consigo encontrar uma definição do que significa 'ponteiro de função compatível'.


1
Sou um tanto novato, mas o que significa um " ponteiro de função void ( ) (void )"? É um ponteiro para uma função que aceita um void * como um argumento e retorna void
Digital Gal

2
@Myke: void (*func)(void *)significa que funcé um ponteiro para uma função com uma assinatura de tipo como void foo(void *arg). Então, sim, você está certo.
mk12

Respostas:


123

No que diz respeito ao padrão C, se você lançar um ponteiro de função para um ponteiro de função de um tipo diferente e depois chamá-lo, é um comportamento indefinido . Consulte o Anexo J.2 (informativo):

O comportamento é indefinido nas seguintes circunstâncias:

  • Um ponteiro é usado para chamar uma função cujo tipo não é compatível com o tipo apontado (6.3.2.3).

Seção 6.3.2.3, parágrafo 8 diz:

Um ponteiro para uma função de um tipo pode ser convertido em um ponteiro para uma função de outro tipo e vice-versa; o resultado deve ser igual ao ponteiro original. Se um ponteiro convertido é usado para chamar uma função cujo tipo não é compatível com o tipo apontado, o comportamento é indefinido.

Em outras palavras, você pode lançar um ponteiro de função para um tipo de ponteiro de função diferente, convertê-lo novamente e chamá-lo, e tudo vai funcionar.

A definição de compatível é um tanto complicada. Pode ser encontrado na seção 6.7.5.3, parágrafo 15:

Para que dois tipos de função sejam compatíveis, ambos devem especificar os tipos de retorno compatíveis 127 .

Além disso, as listas de tipo de parâmetro, se ambas estiverem presentes, devem concordar no número de parâmetros e no uso do terminador de reticências; os parâmetros correspondentes devem ter tipos compatíveis. Se um tipo tem uma lista de tipo de parâmetro e o outro tipo é especificado por um declarador de função que não faz parte de uma definição de função e que contém uma lista de identificadores vazia, a lista de parâmetros não deve ter um terminador de reticências e o tipo de cada parâmetro deve ser compatível com o tipo que resulta da aplicação das promoções de argumento padrão. Se um tipo tem uma lista de tipo de parâmetro e o outro tipo é especificado por uma definição de função que contém uma lista de identificadores (possivelmente vazia), ambos devem concordar no número de parâmetros, e o tipo de cada parâmetro de protótipo deve ser compatível com o tipo que resulta da aplicação das promoções do argumento padrão ao tipo do identificador correspondente. (Na determinação da compatibilidade de tipo e de um tipo composto, cada parâmetro declarado com tipo de função ou array é considerado como tendo o tipo ajustado e cada parâmetro declarado com tipo qualificado é considerado como tendo a versão não qualificada de seu tipo declarado.)

127) Se ambos os tipos de função forem do '' estilo antigo '', os tipos de parâmetro não são comparados.

As regras para determinar se dois tipos são compatíveis estão descritas na seção 6.2.7 e não as citarei aqui, pois são bastante extensas, mas você pode lê-las no rascunho do padrão C99 (PDF) .

A regra relevante aqui está na seção 6.7.5.1, parágrafo 2:

Para que dois tipos de ponteiros sejam compatíveis, ambos devem ser qualificados de forma idêntica e ambos devem ser ponteiros para tipos compatíveis.

Portanto, como a void* não é compatível com a struct my_struct*, um ponteiro de função do tipo void (*)(void*)não é compatível com um ponteiro de função do tipo void (*)(struct my_struct*), portanto, essa conversão de ponteiros de função é tecnicamente um comportamento indefinido.

Na prática, entretanto, você pode se safar com segurança lançando ponteiros de função em alguns casos. Na convenção de chamada x86, os argumentos são colocados na pilha e todos os ponteiros têm o mesmo tamanho (4 bytes em x86 ou 8 bytes em x86_64). Chamar um ponteiro de função se resume a empurrar os argumentos na pilha e fazer um salto indireto para o destino do ponteiro de função e, obviamente, não há noção de tipos no nível do código de máquina.

Coisas que você definitivamente não pode fazer:

  • Converta entre ponteiros de função de diferentes convenções de chamada. Você bagunçará a pilha e, na melhor das hipóteses, travará; na pior, terá sucesso silenciosamente com uma enorme brecha de segurança. Na programação do Windows, você costuma passar ponteiros de função. Win32 espera que todas as funções de retorno de chamada para usar a stdcallconvenção de chamada (que as macros CALLBACK, PASCALe WINAPItodos expandir a). Se você passar um ponteiro de função que usa a convenção de chamada C padrão ( cdecl), o resultado será ruim.
  • Em C ++, converta entre ponteiros de função de membro de classe e ponteiros de função regular. Isso muitas vezes atrapalha novatos em C ++. As funções de membro de classe têm um thisparâmetro oculto e, se você converter uma função de membro em uma função regular, não haverá nenhum thisobjeto a ser usado e, novamente, muita maldade resultará.

Outra má ideia que às vezes pode funcionar, mas também é um comportamento indefinido:

  • Conversão entre ponteiros de função e ponteiros regulares (por exemplo, conversão de a void (*)(void)para a void*). Ponteiros de função não são necessariamente do mesmo tamanho que ponteiros regulares, pois em algumas arquiteturas eles podem conter informações contextuais extras. Isso provavelmente funcionará bem no x86, mas lembre-se de que é um comportamento indefinido.

19
A questão toda não void*é que eles são compatíveis com qualquer outro ponteiro? Não deve haver nenhum problema em converter de a struct my_struct*para a void*, na verdade, você nem deveria ter que lançar, o compilador deve apenas aceitar. Por exemplo, se você passar a struct my_struct*para uma função que recebe a void*, não é necessário converter. O que estou perdendo aqui que os torna incompatíveis?
brianmearns

2
Essa resposta faz referência a "Isso provavelmente funcionará bem no x86 ...": Há alguma plataforma onde NÃO funcionará? Alguém tem experiência quando isso falhou? qsort () para C parece um bom lugar para lançar um ponteiro de função, se possível.
kevinarpe

4
@KCArpe: De acordo com o gráfico sob o título "Implementações de ponteiros de função de membro" neste artigo , o compilador OpenWatcom de 16 bits às vezes usa um tipo de ponteiro de função maior (4 bytes) do que o tipo de ponteiro de dados (2 bytes) em certas configurações. No entanto, os sistemas em conformidade com POSIX devem usar a mesma representação para os void*tipos de ponteiro de função, consulte as especificações .
Adam Rosenfield

3
O link de @adam agora se refere à edição 2016 do padrão POSIX, onde a seção 2.12.3 relevante foi removida. Você ainda pode encontrá-lo na edição de 2008 .
Martin Trenkmann

7
@brianmearns Não, void *é apenas "compatível com" qualquer outro ponteiro (não funcional) de maneiras definidas com muita precisão (que não estão relacionadas ao que o padrão C significa com a palavra "compatível" neste caso). C permite que void *a seja maior ou menor que a struct my_struct *, ou tenha os bits em ordem diferente ou negados ou o que for. Portanto, void f(void *)e void f(struct my_struct *)pode ser incompatível com ABI . C irá converter os próprios ponteiros para você, se necessário, mas não irá e às vezes não poderá converter uma função apontada para obter um tipo de argumento possivelmente diferente.
mtraceur

32

Eu perguntei sobre esse mesmo problema em relação a algum código no GLib recentemente. (GLib é uma biblioteca central para o projeto GNOME e escrita em C.) Disseram-me que todo o framework slots'n'signals depende disso.

Em todo o código, existem inúmeras instâncias de conversão do 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)

É comum fazer encadeamento com chamadas como esta:

Veja você mesmo aqui em g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

As respostas acima são detalhadas e provavelmente corretas - se você fizer parte do comitê de padrões. Adam e Johannes merecem crédito por suas respostas bem pesquisadas. No entanto, lá fora, você descobrirá que este código funciona muito bem. Controverso? Sim. Considere o seguinte: GLib compila / funciona / testa em um grande número de plataformas (Linux / Solaris / Windows / OS X) com uma ampla variedade de compiladores / linkers / carregadores de kernel (GCC / CLang / MSVC). Padrões que se danem, eu acho.

Passei algum tempo pensando nessas respostas. Aqui está minha conclusão:

  1. Se você estiver escrevendo uma biblioteca de retorno de chamada, pode estar tudo bem. Caveat emptor - use por sua própria conta e risco.
  2. Senão, não faça isso.

Pensando mais profundamente depois de escrever esta resposta, não ficaria surpreso se o código para compiladores C usasse esse mesmo truque. E uma vez que (a maioria / todos?) Os compiladores C modernos são inicializados, isso implicaria que o truque é seguro.

Uma questão mais importante para pesquisar: alguém pode encontrar uma plataforma / compilador / linker / carregador onde esse truque não funcione? Pontos principais de brownie para aquele. Aposto que existem alguns processadores / sistemas embarcados que não gostam. No entanto, para a computação desktop (e provavelmente celular / tablet), esse truque provavelmente ainda funciona.


10
Um lugar onde definitivamente não funciona é o compilador Emscripten LLVM para Javascript. Consulte github.com/kripken/emscripten/wiki/Asm-pointer-casts para obter detalhes.
Ben Lings,

2
Referência atualizada sobre o Emscripten .
ysdx

4
O link @BenLings postado será interrompido em um futuro próximo. Ele foi oficialmente movido para kripken.github.io/emscripten-site/docs/porting/guidelines/…
Alex Reinking

10

A questão realmente não é se você pode. A solução trivial é

Um bom compilador só gerará código para my_callback_helper se for realmente necessário, caso em que você ficaria feliz por isso.


O problema é que essa não é uma solução geral. Precisa ser feito caso a caso com conhecimento da função. Se você já tem uma função do tipo errado, está preso.
BeeOnRope

Todos os compiladores com os quais testei irão gerar código my_callback_helper, a menos que esteja sempre embutido. Definitivamente, isso não é necessário, pois a única coisa que tende a fazer é jmp my_callback_function. O compilador provavelmente deseja ter certeza de que os endereços das funções são diferentes, mas infelizmente ele faz isso mesmo quando a função está marcada com C99 inline(ou seja, "não se preocupe com o endereço").
aaaa

Não tenho certeza se isso está correto. Outro comentário de outra resposta acima (por @mtraceur) diz que a void *pode ser até mesmo de tamanho diferente de a struct *(acho que está errado, porque de outra forma mallocestaria quebrado, mas esse comentário tem 5 votos positivos, então estou dando algum crédito. Se @mtraceur estiver certo, a solução que você escreveu não seria correta.
cesss

@cesss: Não importa se o tamanho é diferente. A conversão de e para void*ainda tem que funcionar. Resumindo, void*pode ter mais bits, mas se você converter a struct*para void*esses bits extras, os bits extras poderão ser zeros e a conversão de volta poderá simplesmente descartar esses zeros novamente.
MSalters

@MSalters: Eu realmente não sabia que um void *poderia (em teoria) ser tão diferente de um struct *. Estou implementando uma vtable em C e usando um thisponteiro C ++ - ish como o primeiro argumento para funções virtuais. Obviamente, thisdeve ser um ponteiro para a estrutura "atual" (derivada). Então, funções virtuais precisam de protótipos diferentes dependendo da estrutura em que são implementadas. Pensei que usar um void *thisargumento resolveria tudo, mas agora aprendi que é um comportamento indefinido ...
cesss

6

Você tem um tipo de função compatível se o tipo de retorno e os tipos de parâmetro forem compatíveis - basicamente (é mais complicado na realidade :)). Compatibilidade é o mesmo que "mesmo tipo", apenas mais frouxa para permitir ter tipos diferentes, mas ainda tem alguma forma de dizer "esses tipos são quase iguais". No C89, por exemplo, duas estruturas eram compatíveis se fossem idênticas, mas apenas o nome fosse diferente. C99 parece ter mudado isso. Citando o documento de justificativa c (leitura altamente recomendada, aliás!):

Declarações de tipo de estrutura, união ou enumeração em duas unidades de tradução diferentes não declaram formalmente o mesmo tipo, mesmo se o texto dessas declarações vier do mesmo arquivo de inclusão, uma vez que as unidades de tradução são elas próprias disjuntas. A Norma, portanto, especifica regras de compatibilidade adicionais para tais tipos, de modo que se duas dessas declarações forem suficientemente semelhantes, elas serão compatíveis.

Dito isso - sim, estritamente, este é um comportamento indefinido, porque sua função do_stuff ou outra pessoa irá chamar sua função com um ponteiro de função tendo void*como parâmetro, mas sua função tem um parâmetro incompatível. Mesmo assim, espero que todos os compiladores o compilem e executem sem reclamar. Mas você pode fazer mais limpo tendo outra função tomando uma void*(e registrando-a como função de retorno de chamada) que apenas chamará sua função real.


4

Como o código C compila para uma instrução que não se preocupa com os tipos de ponteiro, é muito bom usar o código que você mencionou. Você teria problemas quando executasse do_stuff com sua função de retorno de chamada e apontasse para algo diferente da estrutura my_struct como argumento.

Espero poder deixar isso mais claro, mostrando o que não funcionaria:

ou...

Basicamente, você pode lançar ponteiros para o que quiser, contanto que os dados continuem a fazer sentido em tempo de execução.


0

Se você pensar sobre como as chamadas de função funcionam em C / C ++, elas colocam certos itens na pilha, saltam para o novo local do código, executam e, em seguida, devolvem a pilha. Se seus ponteiros de função descrevem funções com o mesmo tipo de retorno e o mesmo número / tamanho de argumentos, você deve estar bem.

Portanto, acho que você deve ser capaz de fazer isso com segurança.


2
você só estará seguro enquanto struct-pointers e void-pointers tiverem representações de bits compatíveis; não é garantido que seja o caso
Christoph

1
Compiladores também podem passar argumentos em registradores. E não é incomum usar diferentes registradores para floats, ints ou ponteiros.
MSalters de

0

Os ponteiros nulos são compatíveis com outros tipos de ponteiros. É a espinha dorsal de como malloc e as funções mem ( memcpy, memcmp) funcionam. Normalmente, em C (em vez de C ++) NULLé uma macro definida como ((void *)0).

Olhe para 6.3.2.3 (Item 1) em C99:

Um ponteiro para o vazio pode ser convertido de ou para um ponteiro para qualquer tipo de objeto ou incompleto


Isso contradiz a resposta de Adam Rosenfield , consulte o último parágrafo e comentários
usuário

1
Esta resposta está claramente errada. Qualquer ponteiro pode ser convertido em um ponteiro void, exceto os ponteiros de função.
marton78
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.