c# - ¿Por qué las llamadas de Cdecl a menudo no coinciden en la Convención de Invocación P "estándar"?
c++ pinvoke (2)
Estoy trabajando en una base de código bastante grande en la que la funcionalidad de C ++ es P / invocada desde C #.
Hay muchas llamadas en nuestra base de código, como ...
C ++:
extern "C" int __stdcall InvokedFunction(int);
Con un C # correspondiente:
[DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern int InvokedFunction(IntPtr intArg);
He buscado la red (en la medida en que soy capaz) por el razonamiento de por qué existe esta aparente discordancia. Por ejemplo, ¿por qué hay un Cdecl dentro de C # y __stdcall dentro de C ++? Aparentemente, esto da como resultado que la pila se borre dos veces, pero, en ambos casos, las variables se insertan en la pila en el mismo orden inverso, de modo que no veo ningún error, aunque la posibilidad de que la información de retorno se borre en caso de intentar un seguimiento durante la depuración?
Desde MSDN: http://msdn.microsoft.com/en-us/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);
Una vez más, hay tanto extern "C"
en el código de C ++, como CallingConvention.Cdecl
en C #. ¿Por qué no es CallingConvention.Stdcall
? O, además, ¿por qué hay __stdcall
en C ++?
¡Gracias por adelantado!
Esto aparece repetidamente en las preguntas de SO, intentaré convertir esto en una respuesta de referencia (larga). El código de 32 bits contiene una larga historia de convenciones de llamadas incompatibles. Las opciones sobre cómo hacer una llamada a la función tuvieron sentido hace mucho tiempo, pero en la actualidad son un dolor enorme en la parte trasera. El código de 64 bits tiene una sola convención de llamadas, quien quiera agregar otra será enviado a una isla pequeña en el Atlántico Sur.
Trataré de anotar esa historia y relevancia de ellos más allá de lo que está en el artículo de Wikipedia . El punto de partida es que las opciones que hay que tomar para realizar una llamada a la función son el orden en el que se transfieren los argumentos, dónde almacenar los argumentos y cómo realizar la limpieza después de la llamada.
__stdcall
encontró su camino en la programación de Windows a través de la antigua convención de llamadas__stdcall
16 bits, utilizada en Windows de 16 bits y OS / 2. Es la convención utilizada por todas las funciones de API de Windows, así como COM. Como la mayoría de los pinvoke estaban destinados a realizar llamadas al sistema operativo, Stdcall es el valor predeterminado si no se especifica explícitamente en el atributo [DllImport]. Su única razón de existencia es que especifica que el callee limpia. Lo que produce un código más compacto, muy importante en la época en que tenían que exprimir un sistema operativo GUI en 640 kilobytes de RAM. Su mayor desventaja es que es peligroso . Una discrepancia entre lo que el que llama asume son los argumentos para una función y lo que el callee implementado hace que la pila se desequilibre. Que a su vez puede causar bloqueos extremadamente difíciles de diagnosticar.__cdecl
es la convención de llamadas estándar para el código escrito en el lenguaje C. Su razón principal de existencia es que admite hacer llamadas a funciones con un número variable de argumentos. Común en código C con funciones como printf () y scanf (). Con el efecto secundario de que, dado que es la persona que llama la que sabe cuántos argumentos se pasaron realmente, es la persona que llama la que lo limpia. Olvidar CallingConvention = CallingConvention.Cdecl en la declaración [DllImport] es un error muy común.__fastcall
es una convención de llamadas bastante pobremente definida con elecciones mutuamente incompatibles. Era común en los compiladores de Borland, una empresa que una vez tuvo mucha influencia en la tecnología del compilador hasta que se desintegró. También el ex empleador de muchos empleados de Microsoft, incluyendo a Anders Hejlsberg de C # fame. Fue inventado para hacer que el argumento sea más barato al pasar algunos de ellos a través de registros de CPU en lugar de la pila. No es compatible con el código administrado debido a la baja estandarización.__thiscall
es una convención de llamadas inventada para el código C ++. Muy similar a __cdecl pero también especifica cómo se pasa el puntero oculto de este objeto de clase a los métodos de instancia de una clase. Un detalle adicional en C ++ más allá de C. Aunque parece simple de implementar, el .NET pinvoke marshaller no lo admite. Una de las principales razones por las que no se puede pinvokear el código C ++. La complicación no es la convención de llamadas, es el valor correcto de este puntero. Que puede ser muy intrincado debido al soporte de C ++ para la herencia múltiple. Solo un compilador de C ++ puede averiguar qué es exactamente lo que se debe pasar. Y solo el mismo compilador de C ++ que generó el código para la clase C ++, diferentes compiladores han tomado diferentes decisiones sobre cómo implementar MI y cómo optimizarlo.__clrcall
es la convención de llamadas para código administrado. Es una mezcla de los otros, este puntero pasa como __thiscall, el argumento optimizado pasa como __fastcall, el orden de los argumentos como __cdecl y la limpieza de la persona que llama como __stdcall. La gran ventaja del código administrado es el verificador integrado en el jitter. Lo que asegura que nunca puede haber una incompatibilidad entre quien llama y quien llama. De este modo, los diseñadores pueden aprovechar las ventajas de todas estas convenciones pero sin el problema. Un ejemplo de cómo el código administrado podría seguir siendo competitivo con el código nativo a pesar de la sobrecarga de hacer que el código sea seguro.
Mencionas extern "C"
, entender la importancia de eso también es importante para sobrevivir interoperabilidad. Los compiladores de lenguaje a menudo decoran los nombres de la función exportada con caracteres adicionales. También se llama "name mangling". Es un truco bastante horrible que nunca deja de causar problemas. Y debe comprenderlo para determinar los valores adecuados de las propiedades CharSet, EntryPoint y ExactSpelling de un atributo [DllImport]. Hay muchas convenciones:
Decoración de api de Windows. Windows originalmente era un sistema operativo que no era Unicode, que usaba codificación de 8 bits para cadenas. Windows NT fue el primero que se convirtió en Unicode en su núcleo. Eso causó un problema de compatibilidad bastante importante, el código antiguo no se podría haber ejecutado en sistemas operativos nuevos, ya que pasaría cadenas codificadas de 8 bits a funciones winapi que esperan una cadena Unicode codificada en utf-16. Lo resolvieron escribiendo dos versiones de cada función de winapi. Uno que toma cadenas de 8 bits, otro que toma cadenas Unicode. Y distinguió entre los dos al pegar la letra A al final del nombre de la versión heredada (A = Ansi) y una W al final de la nueva versión (W = ancho). No se agrega nada si la función no toma una cadena. El marshaller de Pinvoke maneja esto automáticamente sin su ayuda, simplemente intentará encontrar las 3 versiones posibles. Sin embargo, siempre debe especificar CharSet.Auto (o Unicode), la sobrecarga de la función heredada que traduce la cadena de Ansi a Unicode es innecesaria y con pérdida.
La decoración estándar para las funciones de __stdcall es _foo @ 4. Subrayado inicial y un postfijo @n que indica el tamaño combinado de los argumentos. Este postfix fue diseñado para ayudar a resolver el desagradable problema de desequilibrio de la pila si la persona que llama y quien llama no están de acuerdo con la cantidad de argumentos. Funciona bien, aunque el mensaje de error no es excelente, el Marshaller de Pinvoke le dirá que no puede encontrar el punto de entrada. Es notable que Windows, al usar __stdcall, no usa esta decoración. Eso fue intencional, dando a los programadores la posibilidad de obtener el argumento GetProcAddress () correcto. El marshaller pinvoke también se encarga de esto automáticamente, primero tratando de encontrar el punto de entrada con el postf @n, y luego probando el que no tiene.
La decoración estándar para la función __cdecl es _foo. Un único guion bajo inicial. El marshaller de Pinvoke ordena esto automáticamente. Tristemente, el postfijo @n opcional para __stdcall no le permite decir que su propiedad CallingConvention es incorrecta, gran pérdida.
Los compiladores de C ++ usan el nombre de manipulación, produciendo nombres que parecen realmente extraños como "?? 2 @ YAPAXI @ Z", el nombre exportado para "operador nuevo". Este fue un mal necesario debido a su soporte para la sobrecarga de funciones. Y originalmente se había diseñado como un preprocesador que utilizaba herramientas heredadas del lenguaje C para construir el programa. Lo que hizo necesario distinguir entre, por ejemplo, una sobrecarga
void foo(char)
yvoid foo(int)
dándoles diferentes nombres. Aquí es donde entra en juego la sintaxisextern "C"
, le dice al compilador de C ++ que no aplique el nombre que cambia al nombre de la función. La mayoría de los programadores que escriben código de interoperabilidad lo usan intencionalmente para hacer que la declaración en el otro idioma sea más fácil de escribir. Lo que en realidad es un error, la decoración es muy útil para detectar desajustes. Utilizaría el archivo .map del enlazador o la utilidad Dumpbin.exe / exports para ver los nombres decorados. La utilidad undname.exe SDK es muy útil para convertir un nombre destruido a su declaración original de C ++.
Entonces esto debería aclarar las propiedades. Utiliza EntryPoint para dar el nombre exacto de la función exportada, una que podría no ser una buena coincidencia para lo que desea llamar en su propio código, especialmente para los nombres anulados de C ++. Y usa ExactSpelling para indicar al marshaller de Pinvoke que no intente encontrar los nombres alternativos porque ya dio el nombre correcto.
Voy a alimentar mi calambre de escritura por un tiempo ahora. La respuesta al título de su pregunta debe ser clara, Stdcall es el valor predeterminado, pero no coincide con el código escrito en C o C ++. Y su declaración [DllImport] no es compatible. Esto debería producir una advertencia en el depurador del Asistente del depurador administrado PInvokeStackImbalance, una extensión de depuración que fue diseñada para detectar malas declaraciones. Y puede bloquear su código de forma aleatoria, especialmente en la compilación Release. Asegúrate de que no apagaste el MDA.
cdecl
y stdcall
son válidos y utilizables entre C ++ y .NET, pero deben ser consistentes entre los dos mundos administrados y no administrados. Por lo tanto, su declaración C # para InvokedFunction no es válida. Debería ser stdcall. La muestra de MSDN simplemente da dos ejemplos diferentes, uno con stdcall (MessageBeep) y otro con cdecl (printf). No están relacionados.