¿Por qué las funciones C no se pueden alterar por nombre?

136

Recientemente tuve una entrevista y una pregunta que se hizo fue para qué sirve el extern "C"código C ++. Respondí que es para usar funciones C en código C ++ ya que C no usa el cambio de nombre. Me preguntaron por qué C no usa el cambio de nombre y, para ser sincero, no pude responder.

Entiendo que cuando el compilador de C ++ compila funciones, le da un nombre especial a la función principalmente porque podemos tener funciones sobrecargadas del mismo nombre en C ++ que deben resolverse en el momento de la compilación. En C, el nombre de la función permanecerá igual, o tal vez con un _ antes.

Mi consulta es: ¿qué hay de malo en permitir que el compilador de C ++ también altere las funciones de C? Asumiría que no importa qué nombres les dé el compilador. Llamamos funciones de la misma manera en C y C ++.

Ingeniero999
fuente
75
C no necesita alterar los nombres, porque no tiene sobrecarga de funciones.
EOF
9
¿Cómo se vinculan las bibliotecas C con el código C ++ si el compilador de C ++ manipula los nombres de las funciones?
Mat
66
"Respondí que es para usar las funciones de C en el código de C ++ ya que C no usa el cambio de nombre". - Creo que es al revés. Extern "C" hace que las funciones de C ++ sean utilizables en un compilador de C. fuente
rozina
3
@ Engineer999: Y si compila el subconjunto de C que también es C ++ con un compilador de C ++, los nombres de las funciones se destrozarán. Pero si desea poder vincular los archivos binarios creados con diferentes compiladores, no desea cambiar el nombre.
EOF
13
C hace nombres mangle. Por lo general, el nombre destrozado es el nombre de la función precedida por un guión bajo. A veces es el nombre de la función seguido de un guión bajo. extern "C"dice que destrozar el nombre de la misma manera que "el" compilador de C lo haría.
Pete Becker

Respuestas:

187

Más o menos fue respondida anteriormente, pero intentaré poner las cosas en contexto.

Primero, C vino primero. Como tal, lo que hace C es, más o menos, el "predeterminado". No destruye nombres porque simplemente no lo hace. Un nombre de función es un nombre de función. Un global es un global, y así sucesivamente.

Entonces apareció C ++. C ++ quería poder usar el mismo enlazador que C, y poder enlazar con el código escrito en C. Pero C ++ no podía dejar el C "destrozando" (o falta de) como está. Mira el siguiente ejemplo:

int function(int a);
int function();

En C ++, estas son funciones distintas, con cuerpos distintos. Si ninguno de ellos está destrozado, ambos se denominarán "función" (o "_función") y el vinculador se quejará de la redefinición de un símbolo. La solución de C ++ consistía en destrozar los tipos de argumentos en el nombre de la función. Entonces, uno se llama _function_inty el otro se llama _function_void(no es un esquema de cambio real) y se evita la colisión.

Ahora nos queda un problema. Si int function(int a)se definió en un módulo C, y simplemente tomamos su encabezado (es decir, declaración) en código C ++ y lo usamos, el compilador generará una instrucción para que el vinculador lo importe _function_int. Cuando se definió la función, en el módulo C, no se llamaba así. Fue llamado _function. Esto provocará un error de vinculador.

Para evitar ese error, durante la declaración de la función, le decimos al compilador que es una función diseñada para ser vinculada o compilada por un compilador de C:

extern "C" int function(int a);

El compilador de C ++ ahora sabe importar _functionmás que _function_int, y todo está bien.

Shachar Shemesh
fuente
1
@ShacharShamesh: He preguntado esto en otra parte, pero, ¿qué pasa con el enlace en bibliotecas compiladas en C ++? Cuando el compilador está avanzando y compilando mi código que llama a una de las funciones en una biblioteca compilada en C ++, ¿cómo sabe qué nombre alterar o asignar a la función al ver su declaración o llamada de función? ¿Cómo saber que donde se define, se le da nombre a otra cosa? Entonces, ¿debe haber un método estándar de cambio de nombre en C ++?
Engineer999
2
Cada compilador lo hace a su manera especial. Si está compilando todo con el mismo compilador, no importa. Pero si intenta utilizar, por ejemplo, una biblioteca que se compiló con el compilador de Borland, de un programa que está creando con el compilador de Microsoft, bueno ... buena suerte; lo necesitarás :)
Mark VY
66
@ Engineer999 ¿Alguna vez se preguntó por qué no existen las bibliotecas C ++ portátiles, pero especifican exactamente qué versión (y marcas) del compilador (y biblioteca estándar) tiene que usar o simplemente exportan una API C? Ahí tienes. C ++ es prácticamente el lenguaje menos portátil jamás inventado, mientras que C es exactamente lo contrario. Hay esfuerzos en ese sentido, pero por ahora si quieres algo que sea realmente portátil, te quedarás con C.
Voo
1
@Voo Bueno, en teoría, debería poder escribir código portátil simplemente adhiriéndose al estándar -std=c++11, por ejemplo , y evitar el uso de cualquier cosa fuera del estándar. Es lo mismo que declarar una versión de Java (aunque las versiones más recientes de Java son compatibles con versiones anteriores). No es culpa de los estándares que las personas usan extensiones específicas del compilador y código dependiente de la plataforma. Por otro lado, no puedes culparlos, ya que hay muchas cosas (especialmente IO, como enchufes) que faltan en el estándar. El comité parece estar poniéndose al día lentamente. Corrígeme si me perdí algo.
mucaho
14
@mucaho: estás hablando de la portabilidad / compatibilidad de la fuente. es decir, la API. Voo está hablando de compatibilidad binaria , sin una nueva compilación. Esto requiere compatibilidad ABI . Los compiladores de C ++ cambian regularmente su ABI entre versiones. (por ejemplo, g ++ ni siquiera intenta tener un ABI estable. Supongo que no rompen el ABI solo por diversión, pero no evitan los cambios que requieren un cambio de ABI cuando hay algo que ganar y no hay otra buena manera para hacerlo.).
Peter Cordes
45

No es que "no puedan", no lo son , en general.

Si desea llamar a una función en una biblioteca de C llamada foo(int x, const char *y), no es bueno dejar que su compilador de C ++ lo manipule foo_I_cCP()(o lo que sea, simplemente creó un esquema de manipulación en el lugar aquí) simplemente porque puede.

Ese nombre no se resolverá, la función está en C y su nombre no depende de su lista de tipos de argumentos. Por lo tanto, el compilador de C ++ tiene que saber esto y marcar esa función como C para evitar hacer el cambio.

Recuerde que dicha función C podría estar en una biblioteca cuyo código fuente no tiene, todo lo que tiene es el binario precompilado y el encabezado. Por lo tanto, su compilador de C ++ no puede hacer "lo suyo", no puede cambiar lo que hay en la biblioteca después de todo.

relajarse
fuente
Esta es la parte que me falta. ¿Por qué el compilador de C ++ destrozaría un nombre de función cuando ve su declaración solo o ve que se llama? ¿No solo altera los nombres de las funciones cuando ve su implementación? Esto tendría más sentido para mí
Engineer999
13
@ Engineer999: ¿Cómo puede tener un nombre para la definición y otro para la declaración? "Hay una función llamada Brian a la que puedes llamar". "Está bien, llamaré a Brian". "Lo siento, no hay una función llamada Brian". Resulta que se llama Graham.
Carreras de ligereza en órbita
¿Qué pasa con los enlaces en bibliotecas compiladas en C ++? Cuando el compilador está avanzando y compilando nuestro código que llama a una de las funciones en una biblioteca compilada en C ++, ¿cómo sabe qué nombre alterar o asignar a la función al ver su declaración o llamada de función?
Ingeniero999
1
@ Engineer999 Ambos deben ponerse de acuerdo sobre la misma destrucción. Entonces ven el archivo de encabezado (recuerde, hay muy pocos metadatos en las DLL nativas, los encabezados son esos metadatos) y dicen "Ah, claro, Brian realmente debería ser Graham". Si esto no funciona (p. Ej., Con dos esquemas de manipulación incompatibles), no obtendrá un enlace correcto y su aplicación fallará. C ++ tiene muchas incompatibilidades como esta. En la práctica, debe usar explícitamente el nombre destrozado y deshabilitar la manipulación en su lado (por ejemplo, le dice a su código que ejecute Graham, no Brian). En la práctica real ... extern "C":)
Luaan
1
@ Engineer999 Podría estar equivocado, pero ¿quizás tenga experiencia con lenguajes como Visual Basic, C # o Java (o incluso Pascal / Delphi hasta cierto punto)? Esos hacen que la interoperabilidad parezca extremadamente simple. En C y especialmente en C ++, es todo lo contrario. Hay muchas convenciones de llamadas que debe cumplir, debe saber quién es responsable de qué memoria y debe tener los archivos de encabezado que le indiquen las declaraciones de funciones, ya que las DLL en sí no contienen suficiente información, especialmente en el caso de C. puro. Si no tiene un archivo de encabezado, generalmente necesita descompilar el archivo DLL para usarlo.
Luaan
32

¿Qué tiene de malo permitir que el compilador de C ++ altere las funciones de C también?

Ya no serían funciones de C.

Una función no es solo una firma y una definición; cómo funciona una función está determinada en gran medida por factores como la convención de llamada. La "Interfaz binaria de aplicación" especificada para su uso en su plataforma describe cómo los sistemas se comunican entre sí. El ABI de C ++ en uso por su sistema especifica un esquema de cambio de nombre, para que los programas en ese sistema sepan cómo invocar funciones en bibliotecas, etc. (Lea el C ++ Itanium ABI para ver un excelente ejemplo. Muy rápidamente verá por qué es necesario).

Lo mismo se aplica para el C ABI en su sistema. Algunas C ABI realmente tienen un esquema de cambio de nombre (por ejemplo, Visual Studio), por lo que se trata menos de "desactivar el cambio de nombre" y más sobre cambiar de C ++ ABI a C ABI, para ciertas funciones. Marcamos las funciones de C como funciones de C, para lo cual el C ABI (en lugar del C ++ ABI) es pertinente. La declaración debe coincidir con la definición (ya sea en el mismo proyecto o en alguna biblioteca de terceros), de lo contrario, la declaración no tiene sentido. Sin eso, su sistema simplemente no sabrá cómo localizar / invocar esas funciones.

En cuanto a por qué las plataformas no definen las ABI de C y C ++ como iguales y se deshacen de este "problema", eso es parcialmente histórico: las ABI de C originales no fueron suficientes para C ++, que tiene espacios de nombres, clases y sobrecarga de operadores, todo de los cuales deben representarse de alguna manera en el nombre de un símbolo de una manera amigable con la computadora, pero también se podría argumentar que hacer que los programas C ahora cumplan con C ++ es injusto para la comunidad C, que tendría que soportar una enorme complejidad ABI solo por el bien de otras personas que desean interoperabilidad.

Carreras de ligereza en órbita
fuente
2
+int(PI/3), pero con un grano de sal: sería muy cauteloso al hablar de "C ++ ABI" ... AFAIK, hay intentos de definir ABI de C ++, pero no hay estándares reales de facto / de jure , como isocpp.org/files /papers/n4028.pdf declara (y estoy totalmente de acuerdo), cita, es profundamente irónico que C ++ siempre haya apoyado una forma de publicar una API con un ABI binario estable, recurriendo al subconjunto C de C ++ a través de "C" externo ". . C++ Itanium ABIes solo eso: algo de C ++ ABI para Itanium ... como se discutió en stackoverflow.com/questions/7492180/c-abi-issues-list
3
@vaxquis: Sí, no "ABI de C ++", sino "un ABI de C ++" de la misma manera que tengo una "clave de la casa" que no funciona en todas las casas. Supongo que podría ser más claro, aunque traté de hacerlo lo más claro posible comenzando con la frase "El C ++ ABI en uso por su sistema " . Dejé caer el clarificador en declaraciones posteriores por brevedad, ¡pero aceptaré una edición que reduce la confusión aquí!
Carreras de ligereza en órbita el
1
AIUI C abi tiende a ser una propiedad de una plataforma, mientras que C ++ ABI tiende a ser una propiedad de un compilador individual y, a menudo, incluso una propiedad de una versión individual de un compilador. Entonces, si quería vincular entre módulos construidos con diferentes herramientas de proveedores, tenía que usar un C abi para la interfaz.
lavar el
La afirmación "las funciones de nombre mutilado ya no serían funciones de C" es exagerada: es perfectamente posible llamar a funciones de nombre mutilado de C simple si se conoce el nombre de miembro. Que los cambios de nombre no lo hagan menos adherente al C ABI, es decir, no lo hace menos una función C. A la inversa, tiene más sentido: el código C ++ no puede llamar a una función C sin declararla "C" porque sería un cambio de nombre al intentar vincularse contra el destinatario.
Peter - Restablece a Mónica el
@ PeterA.Schneider: Sí, la frase principal es exagerada. El resto de la respuesta contiene los detalles de hecho pertinentes.
Carreras de ligereza en órbita
21

De hecho , MSVC destroza los nombres de C, aunque de una manera simple. A veces se agrega @4u otro número pequeño. Esto se relaciona con las convenciones de llamadas y la necesidad de limpieza de la pila.

Entonces, la premisa es simplemente defectuosa.

MSalters
fuente
2
Eso no es realmente un cambio de nombre. Es simplemente una convención de nomenclatura (o adorno de nombre) específica del proveedor para evitar problemas con los ejecutables que se vinculan a archivos DLL creados con las funciones que tienen diferentes convenciones de llamadas.
Peter
2
¿Qué hay de pretender con un _?
OrangeDog
12
@Peter: Literalmente lo mismo.
Carreras de ligereza en órbita
55
@Frankie_C: "La persona que llama limpia la pila" no está especificada por ningún estándar C: ninguna convención de llamada es más estándar que la otra desde una perspectiva de lenguaje.
Ben Voigt
2
Y desde una perspectiva MSVC, la "convención de llamadas estándar" es justo lo que usted elige /Gd, /Gr, /Gv, /Gz. (Es decir, la convención de llamada estándar es lo que se usa a menos que una declaración de función especifique explícitamente una convención de llamada). Estás pensando __cdeclcuál es la convención de llamadas estándar predeterminada.
MSalters
13

Es muy común tener programas que están parcialmente escritos en C y parcialmente escritos en algún otro lenguaje (a menudo lenguaje ensamblador, pero a veces Pascal, FORTRAN u otra cosa). También es común que los programas contengan diferentes componentes escritos por diferentes personas que pueden no tener el código fuente para todo.

En la mayoría de las plataformas, hay una especificación, a menudo llamada ABI [Application Binary Interface] que describe lo que debe hacer un compilador para producir una función con un nombre particular que acepte argumentos de algunos tipos particulares y devuelva un valor de algún tipo particular. En algunos casos, un ABI puede definir más de una "convención de llamada"; Los compiladores para tales sistemas a menudo proporcionan un medio para indicar qué convención de llamada se debe utilizar para una función particular. Por ejemplo, en Macintosh, la mayoría de las rutinas de Toolbox usan la convención de llamadas Pascal, por lo que el prototipo de algo como "LineTo" sería algo como:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

Si todo el código en un proyecto fue compilado usando el mismo compilador, no importaría qué nombre exportó el compilador para cada función, pero en muchas situaciones será necesario que el código C invoque funciones que fueron compiladas usando otras herramientas y no se puede volver a compilar con el compilador actual [y es muy posible que ni siquiera esté en C]. Ser capaz de definir el nombre del enlazador es, por lo tanto, crítico para el uso de tales funciones.

Super gato
fuente
Sí, esa es la respuesta. Si es solo C y C ++, entonces es difícil entender por qué se hace de esa manera. Para entender debemos poner las cosas en el contexto de la antigua forma de vinculación estática. La vinculación estática parece primitiva para los programadores de Windows, pero es la razón principal por la que C no puede alterar los nombres.
user34660
2
@ user34660: No qutie. Es la razón por la que C no puede exigir la existencia de características cuya implementación requeriría cambiar nombres exportables o permitir la existencia de múltiples símbolos con nombres similares que se distinguen por características secundarias.
supercat
¿Sabemos que hubo intentos de "ordenar" tales cosas o que esas extensiones eran extensiones disponibles para C antes de C ++?
user34660
@ user34660: Re "La vinculación estática parece primitiva para los programadores de Windows ...", pero la vinculación dinámica a veces parece una gran PITA para las personas que usan Linux, cuando instalar el programa X (probablemente escrito en C ++) significa tener que rastrear e instalar versiones particulares de bibliotecas de las que ya tiene diferentes versiones en su sistema.
jamesqf
@jamesqf, sí, Unix no tenía enlaces dinámicos antes que Windows. Sé muy poco sobre la vinculación dinámica en Unix / Linux, pero parece que no es tan transparente como podría ser en un sistema operativo en general.
user34660
12

Agregaré otra respuesta, para abordar algunas de las discusiones tangenciales que tuvieron lugar.

El C ABI (interfaz binaria de la aplicación) originalmente solicitó pasar argumentos en la pila en orden inverso (es decir, empujado de derecha a izquierda), donde la persona que llama también libera el almacenamiento de la pila. El ABI moderno en realidad usa registros para pasar argumentos, pero muchas de las consideraciones de cambio se remontan a la aprobación del argumento de pila original.

El ABI Pascal original, por el contrario, empujó los argumentos de izquierda a derecha, y el destinatario tuvo que reventar los argumentos. El C ABI original es superior al Pascal ABI original en dos puntos importantes. El orden de inserción de argumentos significa que siempre se conoce el desplazamiento de la pila del primer argumento, lo que permite funciones que tienen un número desconocido de argumentos, donde los primeros argumentos controlan cuántos otros argumentos hay (ala printf).

La segunda forma en que el C ABI es superior es el comportamiento en caso de que la persona que llama y la persona que llama no estén de acuerdo sobre cuántos argumentos hay. En el caso C, siempre y cuando no acceda a argumentos más allá del último, no pasa nada malo. En Pascal, se saca una cantidad incorrecta de argumentos de la pila y se corrompe toda la pila.

El ABI original de Windows 3.1 estaba basado en Pascal. Como tal, usó el ABI de Pascal (argumentos en orden de izquierda a derecha, pops callee). Dado que cualquier discrepancia en el número de argumento podría conducir a la corrupción de la pila, se formó un esquema de distribución. Cada nombre de función fue destrozado con un número que indica el tamaño, en bytes, de sus argumentos. Entonces, en una máquina de 16 bits, la siguiente función (sintaxis C):

int function(int a)

Fue destrozado function@2porque inttiene dos bytes de ancho. Esto se hizo para que si la declaración y la definición no coinciden, el vinculador no podrá encontrar la función en lugar de dañar la pila en tiempo de ejecución. Por el contrario, si el programa se vincula, puede estar seguro de que el número correcto de bytes aparece de la pila al final de la llamada.

Windows de 32 bits en adelante usa el stdcallABI en su lugar. Es similar al Pascal ABI, excepto que el orden de inserción es como en C, de derecha a izquierda. Al igual que el ABI de Pascal, el cambio de nombre altera el tamaño del byte de argumentos en el nombre de la función para evitar la corrupción de la pila.

A diferencia de las afirmaciones hechas aquí, el C ABI no destruye los nombres de las funciones, incluso en Visual Studio. Por el contrario, las funciones de manipulación decoradas con la stdcallespecificación ABI no son exclusivas de VS. GCC también admite este ABI, incluso cuando compila para Linux. Wine lo utiliza ampliamente , ya que utiliza su propio cargador para permitir la vinculación en tiempo de ejecución de archivos binarios compilados de Linux a archivos DLL compilados de Windows.

Shachar Shemesh
fuente
9

Los compiladores de C ++ usan el cambio de nombre para permitir nombres de símbolos únicos para funciones sobrecargadas cuya firma de otro modo sería la misma. Básicamente, también codifica los tipos de argumentos, lo que permite el polimorfismo en un nivel basado en funciones.

C no requiere esto ya que no permite la sobrecarga de funciones.

Tenga en cuenta que el cambio de nombre es una (¡pero ciertamente no la única!) Razón por la que no se puede confiar en un 'C ++ ABI'.

dgrine
fuente
8

C ++ quiere poder interoperar con el código C que enlaza contra él, o que enlaza contra.

C espera nombres de funciones sin nombre mutilado.

Si C ++ lo destrozara, no encontraría las funciones no destrozadas exportadas de C, o C no encontraría las funciones exportadas de C ++. El enlazador C debe obtener el nombre que espera, ya que no sabe si viene o va a C ++.

Yakk - Adam Nevraumont
fuente
3

Al modificar los nombres de las funciones y variables de C, se podrían verificar sus tipos en el momento del enlace. Actualmente, todas las implementaciones de (?) C le permiten definir una variable en un archivo y llamarlo como una función en otro. O puede declarar una función con una firma incorrecta (por ejemplo, void fopen(double)y luego llamarla.

Propuse un esquema para el enlace de tipo seguro de las variables y funciones de C mediante el uso de la manipulación en 1991. El esquema nunca se adoptó, porque, como otros han señalado aquí, esto destruiría la compatibilidad con versiones anteriores.

Diomidis Spinellis
fuente
1
Se refiere a "permitir que se verifiquen sus tipos en el momento del enlace ". Los tipos se verifican en el momento de la compilación, pero la vinculación con nombres no desglosados ​​no puede verificar si las declaraciones utilizadas en las diferentes unidades de compilación están de acuerdo. Y si no están de acuerdo, es su sistema de compilación el que está fundamentalmente roto y necesita ser reparado.
cmaster - restablecer monica