Por que as chamadas Cdecl muitas vezes são incompatíveis na convenção P / Invoke “padrão”?

Eu estou trabalhando em uma base de código bastante grande em que a funcionalidade de C ++ é P / Invocada de c #.

Existem muitas chamadas em nossa base de código, como …

C ++:

extern "C" int __stdcall InvokedFunction(int); 

Com um correspondente C #:

 [DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] private static extern int InvokedFunction(IntPtr intArg); 

Eu vasculhei a rede (na medida em que sou capaz) pelo raciocínio sobre o porquê dessa aparente incompatibilidade existir. Por exemplo, por que existe um Cdecl dentro do C # e __stdcall dentro do C ++? Aparentemente, isso resulta na pilha sendo limpa duas vezes, mas, em ambos os casos, variables ​​são colocadas na pilha na mesma ordem reversa, de modo que eu não vejo nenhum erro, embora a possibilidade de que a informação de retorno seja limpa no caso de tentando um rastreamento durante a debugging?

No MSDN: http://msdn.microsoft.com/pt-br/library/2x8kf7zx%28v=vs.100%29.aspx

 // explicit DLLImport needed here to use P/Invoke marshalling [DllImport("msvcrt.dll", EntryPoint = "printf", CallingConvention = CallingConvention::Cdecl, CharSet = CharSet::Ansi)] // Implicit DLLImport specifying calling convention extern "C" int __stdcall MessageBeep(int); 

Mais uma vez, há tanto extern "C" no código C ++ e CallingConvention.Cdecl no C #. Por que não é CallingConvention.Stdcall ? Ou, além disso, por que há __stdcall no C ++?

Desde já, obrigado!

   

Isso aparece repetidamente em perguntas do SO, vou tentar transformar isso em uma resposta (longa) de referência. O código de 32 bits está sobrecarregado com um longo histórico de convenções de chamada incompatíveis. Escolhas sobre como fazer uma chamada de function que fazia sentido há muito tempo, mas são na maioria das vezes uma dor gigantesca na retaguarda hoje. O código de 64 bits tem apenas uma convenção de chamada, quem vai adicionar outro vai ser enviado para uma pequena ilha no Atlântico Sul.

Vou tentar anotar essa história e relevância deles além do que está no artigo da Wikipedia . O ponto de partida é que as escolhas a serem feitas em como fazer uma chamada de function são a ordem na qual os argumentos devem ser passados, onde armazenar os argumentos e como limpá-los após a chamada.

  • __stdcall encontrou seu caminho na programação do Windows por meio da antiga convenção de chamada pascal de 16 bits, usada no Windows de 16 bits e no OS / 2. É a convenção usada por todas as funções da API do Windows, bem como pelo COM. Como a maioria dos pinvokes era destinada a fazer chamadas de sistema operacional, Stdcall é o padrão se você não especificar explicitamente no atributo [DllImport]. Sua única razão para a existência é que ela especifica que o receptor se purifica. O que produz código mais compacto, muito importante nos dias em que eles tinham que apertar um sistema operacional GUI em 640 kilobytes de RAM. Sua maior desvantagem é que é perigoso . Uma incompatibilidade entre o que o chamador assume são os argumentos para uma function e o que o receptor implementado faz com que a pilha fique desbalanceada. O que, por sua vez, pode dificultar o diagnóstico de falhas.

  • __cdecl é a convenção de chamada padrão para o código escrito na linguagem C. Seu principal motivo de existência é que ele suporta fazer chamadas de function com um número variável de argumentos. Comum no código C com funções como printf () e scanf (). Com o efeito colateral de que, como é o chamador que sabe quantos argumentos foram realmente passados, é o chamador que limpa. Esquecer CallingConvention = CallingConvention.Cdecl na declaração [DllImport] é um erro muito comum.

  • __fastcall é uma convenção de chamada bem mal definida com opções mutuamente incompatíveis. Era comum nos compiladores da Borland, uma empresa antes muito influente na tecnologia de compiladores até se desintegrar. Também o antigo empregador de muitos funcionários da Microsoft, incluindo Anders Hejlsberg de fama c #. Ele foi inventado para tornar o argumento mais barato passando alguns deles através de registradores de CPU em vez da pilha. Não é suportado no código gerenciado devido à baixa padronização.

  • __thiscall é uma convenção de chamada inventada para o código C ++. Muito semelhante a __cdecl mas também especifica como o ponteiro para um object de class oculto é passado para os methods de instância de uma class. Um detalhe extra em C ++ além de C. Embora pareça simples de implementar, o marshaller pinvoke .NET não suporta isso. Um dos principais motivos que você não pode pinvoke código C ++. A complicação não é a convenção de chamada, é o valor adequado do ponteiro. Que pode ficar muito complicado devido ao suporte do C ++ para inheritance múltipla. Somente um compilador C ++ pode descobrir exatamente o que precisa ser passado. E apenas o mesmo compilador C ++ que gerou o código para a class C ++, diferentes compiladores fizeram escolhas diferentes sobre como implementar o MI e como otimizá-lo.

  • __clrcall é a convenção de chamada para código gerenciado. É uma mistura dos outros, este ponteiro passando como __thiscall, argumento otimizado passando como __fastcall, ordem de argumento como __cdecl e limpeza de chamador como __stdcall. A grande vantagem do código gerenciado é o verificador embutido no jitter. O que garante que nunca haverá uma incompatibilidade entre o chamador e o chamado. Permitindo assim que os projetistas aproveitem as vantagens de todas essas convenções, mas sem a bagagem de problemas. Um exemplo de como o código gerenciado pode permanecer competitivo com o código nativo, apesar da sobrecarga de tornar o código seguro.

Você menciona extern "C" , entendendo o significado disso é importante também para sobreviver interoperabilidade. Os compiladores de linguagem geralmente decoram os nomes das funções exportadas com caracteres extras. Também chamado de “name mangling”. É um truque muito ruim que nunca deixa de causar problemas. E você precisa entendê-lo para determinar os valores adequados das propriedades CharSet, EntryPoint e ExactSpelling de um atributo [DllImport]. Existem muitas convenções:

  • Decoração de api do Windows. O Windows era originalmente um sistema operacional não-Unicode, usando codificação de 8 bits para seqüências de caracteres. O Windows NT foi o primeiro que se tornou o Unicode em seu núcleo. Isso causou um grande problema de compatibilidade, o código antigo não poderia rodar em novos sistemas operacionais, já que passaria strings codificadas de 8 bits para funções winapi que esperam uma string Unicode codificada por utf-16. Eles resolveram isso escrevendo duas versões de cada function winapi. Um que leva seqüências de caracteres de 8 bits, outro que usa seqüências de caracteres Unicode. E distinguiu entre os dois, colando a letra A no final do nome da versão legada (A = Ansi) e um W no final da nova versão (W = wide). Nada é adicionado se a function não levar uma string. O marshaller pinvoke lida com isso automaticamente sem a sua ajuda, ele simplesmente tentará encontrar todas as 3 versões possíveis. No entanto, você deve sempre especificar CharSet.Auto (ou Unicode), a sobrecarga da function legada que converte a string de Ansi para Unicode é desnecessária e com perdas.

  • A decoração padrão para as funções __stdcall é _foo @ 4. Destaque de sublinhado e um @n postfix que indica o tamanho combinado dos argumentos. Este postfix foi projetado para ajudar a resolver o problema do desequilíbrio de pilha desagradável se o chamador e o chamado não concordarem com o número de argumentos. Funciona bem, embora a mensagem de erro não seja ótima, o marshaller pinvoke lhe dirá que não pode encontrar o ponto de input. Notável é que o Windows, ao usar __stdcall, não usa essa decoração. Isso foi intencional, dando aos programadores uma chance de acertar o argumento GetProcAddress (). O marshaller pinvoke também cuida disso automaticamente, primeiro tentando encontrar o ponto de input com o @n postfix, depois tentando o outro sem.

  • A decoração padrão para a function __cdecl é _foo. Um único sublinhado à esquerda. O marshaller pinvoke classifica isso automaticamente. Infelizmente, o postfix @n opcional para __stdcall não permite que você diga que sua propriedade CallingConvention está errada, uma grande perda.

  • Os compiladores de C ++ usam o nome mangling, produzindo nomes realmente bizarros como “2 @ YAPAXI @ Z”, o nome exportado para “operator new”. Este foi um mal necessário devido ao seu suporte para sobrecarga de funções. E originalmente ele foi projetado como um pré-processador que usava ferramentas de linguagem C legadas para fazer o programa ser construído. O que tornava necessário distinguir entre, digamos, uma sobrecarga de void foo(char) e de void foo(int) , dando-lhes nomes diferentes. Este é o lugar onde a syntax extern "C" entra em jogo, ele diz ao compilador C ++ para não aplicar o nome mangling ao nome da function. A maioria dos programadores que escrevem o código de interoperabilidade o usam intencionalmente para facilitar a escrita da declaração na outra língua. O que na verdade é um erro, a decoração é muito útil para pegar desencontros. Você usaria o arquivo .map do vinculador ou o utilitário Dumpbin.exe / exports para ver os nomes decorados. O utilitário undname.exe SDK é muito útil para converter um nome desconfigurado em sua declaração C ++ original.

Então, isso deve esclarecer as propriedades. Você usa EntryPoint para fornecer o nome exato da function exportada, uma que pode não ser uma boa correspondência para o que você deseja chamá-la em seu próprio código, especialmente para nomes desconfigurados em C ++. E você usa ExactSpelling para dizer ao marshaller pinvoke para não tentar encontrar os nomes alternativos, porque você já deu o nome correto.

Eu vou amamentar minha cãibra por um tempo agora. A resposta para o título da pergunta deve ser clara, Stdcall é o padrão, mas é uma incompatibilidade de código escrito em C ou C ++. E a sua declaração [DllImport] não é compatível. Isso deve produzir um aviso no depurador do PInvokeStackImbalance Managed Debugger Assistant, uma extensão de depurador que foi projetada para detectar declarações incorretas. E pode, aleatoriamente, travar seu código, particularmente na versão do Release. Certifique-se de não desligar o MDA.

cdecl e stdcall são válidos e utilizáveis ​​entre C ++ e .NET, mas devem ser consistentes entre os dois mundos não gerenciados e gerenciados. Portanto, sua declaração do C # para InvokedFunction é inválida. Deve ser stdcall. O exemplo MSDN apenas fornece dois exemplos diferentes, um com stdcall (MessageBeep) e outro com cdecl (printf). Eles não estão relacionados.