C ++: falta de estandarización a nivel binario

14

¿Por qué ISO / ANSI no estandarizó C ++ a nivel binario? Hay muchos problemas de portabilidad con C ++, que se debe solo a la falta de estandarización a nivel binario.

Don Box escribe, (citando su libro Essential COM , capítulo COM As A Better C ++ )

C ++ y portabilidad


Una vez que se toma la decisión de distribuir una clase de C ++ como una DLL, uno se enfrenta a una de las debilidades fundamentales de C ++ , es decir, la falta de estandarización a nivel binario . Aunque el documento de trabajo borrador ISO / ANSI C ++ intenta codificar qué programas se compilarán y cuáles serán los efectos semánticos de ejecutarlos, no intenta estandarizar el modelo de tiempo de ejecución binario de C ++. La primera vez que este problema se hará evidente es cuando un cliente intenta vincularse con la biblioteca de importación de la DLL de FastString desde un entorno de desarrollo de C ++ distinto del que se usó para construir la DLL de FastString.

¿Hay más beneficios o pérdida de esta falta de estandarización binaria?

Nawaz
fuente
¿Se pregunta esto mejor en programmers.stackexchange.com , ya que es una pregunta más subjetiva?
Stephen Furlani
1
Pregunta mía relacionada en realidad: stackoverflow.com/questions/2083060/…
AraK
44
Don Box es un fanático. Ignoralo.
John Dibling
8
Bueno, C tampoco está estandarizado por ANSI / ISO en el nivel binario; OTOH C tiene un ABI estándar de facto en lugar de uno de jure . C ++ no tiene una ABI tan estandarizada porque diferentes fabricantes tenían diferentes objetivos con sus implementaciones. Por ejemplo, excepciones en VC ++ piggyback en la parte superior de Windows SEH. POSIX no tiene SEH y, por lo tanto, tomar ese modelo no tendría sentido (por lo tanto, G ++ y MinGW no usan ese modelo).
Billy ONeal
3
Veo esto como una característica, no como una debilidad. Si vincula una implementación a un ABI específico, nunca tendremos innovación y el nuevo hardware estará vinculado al diseño del lenguaje (y dado que hay 15 años entre cada nueva versión, eso es mucho tiempo en la industria del hardware) y sofocando No se crearán nuevas ideas innovadoras para que el código se ejecute de manera más eficiente. El precio es que todo el código en un ejecutable debe ser construido por el mismo compilador / versión (un problema pero no uno importante).

Respuestas:

16

Los lenguajes con forma compilada compatible con binarios son una fase relativamente nueva [*], por ejemplo, los tiempos de ejecución JVM y .NET. Los compiladores C y C ++ generalmente emiten código nativo.

La ventaja es que no hay necesidad de un JIT, o un intérprete de bytecode, o una VM, o cualquier otra cosa. Por ejemplo, no puede escribir el código de arranque que se ejecuta al inicio de la máquina como un código de bytes Java agradable y portátil, a menos que tal vez la máquina pueda ejecutar código de bytes Java de forma nativa, o tenga algún tipo de convertidor de Java a un nativo no compatible con binarios código ejecutable (en teoría: no estoy seguro de que esto se pueda recomendar en la práctica para el código bootstrap). Puede escribirlo en C ++, más o menos, aunque no sea C ++ portátil, incluso en el nivel de origen, ya que hará mucho lío con las direcciones mágicas de hardware.

La desventaja es que, por supuesto, el código nativo solo se ejecuta en la arquitectura para la que se compiló, y los ejecutables solo pueden ser cargados por un cargador que entienda su formato ejecutable, y solo se vinculan y llaman a otros ejecutables para la misma arquitectura y ABI

Incluso si llega tan lejos, vincular dos ejecutables juntos solo funcionará correctamente siempre que: (a) no viole la Regla de una definición, que es fácil de hacer si se compilaron con diferentes compiladores / opciones / lo que sea, de modo que estaban usando diferentes definiciones de la misma clase (ya sea en un encabezado o porque cada uno estaba vinculado estáticamente con diferentes implementaciones); y (b) todos los detalles de implementación relevantes, como el diseño de la estructura, son idénticos de acuerdo con las opciones del compilador vigentes cuando se compiló cada uno.

Para que el estándar C ++ defina todo esto, se eliminarían muchas de las libertades disponibles actualmente para los implementadores. Los implementadores están usando esas libertades, especialmente cuando escriben código de muy bajo nivel en C ++ (y C, que tiene el mismo problema).

Si desea escribir algo que se parezca un poco a C ++, para un objetivo binario portátil, hay C ++ / CLI, que se dirige a .NET y Mono para que pueda (con suerte) ejecutar .NET en otro lugar que no sea Windows. Creo que es posible persuadir al compilador de MS para que produzca ensamblados CIL puros que se ejecutarán en Mono.

También hay cosas potencialmente que se pueden hacer con, por ejemplo, LLVM para crear un entorno C o C ++ portátil binario. Sin embargo, no sé si ha surgido ningún ejemplo generalizado.

Pero todo esto depende de arreglar muchas cosas que C ++ hace que la implementación dependa (como el tamaño de los tipos). Entonces, el entorno que comprende los binarios portátiles debe estar disponible en el sistema donde se ejecutará el código. Al permitir binarios no portátiles, C y C ++ pueden ir a lugares donde los binarios portátiles no pueden, y es por eso que el estándar no dice nada sobre los binarios.

Luego, en cualquier plataforma dada, las implementaciones generalmente aún no proporcionan compatibilidad binaria entre diferentes conjuntos de opciones, aunque el estándar no las detiene. Si a Don Box no le gusta que los compiladores de Microsoft puedan producir archivos binarios incompatibles de la misma fuente, de acuerdo con las opciones del compilador, entonces es el equipo del compilador del que debe quejarse. El lenguaje C ++ no prohíbe que un compilador o un sistema operativo fije todos los detalles necesarios, por lo que una vez que se limita a Windows no es un problema fundamental con C ++. Microsoft ha decidido no hacerlo.

Las diferencias a menudo se manifiestan como una cosa más que puede equivocarse y bloquear su programa, pero puede haber ganancias considerables en la eficiencia entre, por ejemplo, las versiones incompatibles de depuración frente a lanzamiento de un dll.

[*] No estoy seguro de cuándo se inventó la idea, probablemente en 1642 o algo así, pero su popularidad actual es relativamente nueva, en comparación con el momento en que C ++ se comprometió con las decisiones de diseño que impiden definir la portabilidad binaria.

Steve Jessop
fuente
@Steve Pero C tiene un ABI bien definido en i386 y AMD64, por lo que puedo pasar un puntero a una función compilada por GCC versión X a una función compilada por MSVC versión Y. Hacer eso con una función C ++ es imposible.
user877329
7

La compatibilidad entre plataformas y compiladores cruzados no eran los objetivos principales detrás de C y C ++. Nacieron en una era y estaban destinados a propósitos para los cuales las minimizaciones de tiempo y espacio específicas de plataforma y compilador eran cruciales.

Del "Diseño y evolución de C ++" de Stroustrup:

"El objetivo explícito era hacer coincidir C en términos de tiempo de ejecución, compacidad de código y compacidad de datos ... El ideal, que se logró, era que C con Clases podría usarse para cualquier cosa que C pudiera usarse".

Andy Thomas
fuente
1
+1 - exactamente. ¿Cómo se podría construir un ABI estándar que funcionara tanto en cajas ARM como Intel? ¡No tendría sentido!
Billy ONeal
1
desafortunadamente, falló en esto. Puede hacer todo lo que hace C ... excepto cargar dinámicamente un módulo C ++ en tiempo de ejecución. tienes que 'volver' a usar las funciones de C en la interfaz expuesta.
gbjbaanb
6

¡No es un error, es una característica! Esto le da a los implementadores la libertad de optimizar su implementación a nivel binario. El little-endian i386 y su descendencia no son las únicas CPU que tienen o existen.


fuente
6

El problema descrito en la cita es causado por la evitación bastante deliberada de la estandarización de los esquemas de cambio de nombre de símbolo (creo que " estandarización a nivel binario " es una frase engañosa a este respecto, aunque el problema está relacionado con la interfaz binaria de aplicación del compilador ( ABI).

C ++ codifica la información de tipo y firma de un objeto de datos o función, y su membresía de clase / espacio de nombres en el nombre del símbolo, y diferentes compiladores pueden usar diferentes esquemas. En consecuencia, un símbolo en una biblioteca estática, DLL o archivo de objeto no se vinculará con el código compilado usando un compilador diferente (o posiblemente incluso una versión diferente del mismo compilador).

El problema se describe y explica probablemente mejor que yo aquí , con ejemplos de esquemas utilizados por diferentes compiladores.

Las razones de la deliberada falta de estandarización también se explican aquí .

Clifford
fuente
3

El objetivo de ISO / ANSI era estandarizar el lenguaje C ++, problema que parece ser lo suficientemente complejo como para requerir años para tener una actualización de los estándares del lenguaje y el soporte del compilador.

La compatibilidad binaria es mucho más compleja, dado que los binarios deben ejecutarse en diferentes arquitecturas de CPU y diferentes entornos de sistema operativo.


fuente
Es cierto, pero el problema descrito en la cita no tiene nada que ver con la "compatibilidad de nivel binario" (a pesar del uso del término por parte del autor) en ningún sentido, aparte de que esas cosas se definen en algo llamado "Interfaz binaria de aplicación". De hecho, está describiendo la cuestión de los esquemas incompatibles de manipulación de nombres.
@Clifford: el esquema de cambio de nombre es solo un subconjunto de la compatibilidad de nivel binario. ¡el último es más como un término general!
Nawaz
Dudo que haya un problema al intentar ejecutar un binario de Linux en una máquina con Windows. Las cosas serían mucho mejores si hubiera un ABI por plataforma, ya que al menos un lenguaje de script podría cargar y ejecutar dinámicamente un binario en la misma plataforma, o las aplicaciones podrían usar componentes construidos con un compilador diferente. No puede usar un dll C en Linux hoy, y nadie se queja, pero ese dll C aún puede ser cargado por una aplicación de Python, que es donde se acumula el beneficio.
gbjbaanb
2

Como dijo Andy, la compatibilidad multiplataforma no era un gran objetivo, mientras que la implementación de hardware y plataforma amplia era un objetivo, con el resultado neto de que puede escribir implementaciones conformes para una selección muy amplia de sistemas. La estandarización binaria habría hecho esto prácticamente inalcanzable.

La compatibilidad con C también era importante y lo habría complicado significativamente.

Posteriormente se han realizado algunos esfuerzos para estandarizar la ABI para un subconjunto de implementaciones.

Flexografía
fuente
Maldición, olvidé la compatibilidad de C. Buen punto, +1!
Andy Thomas
1

Creo que la falta de un estándar para C ++ es un problema en el mundo actual de la programación modular desacoplada. Sin embargo, tenemos que definir lo que queremos de ese estándar.

Nadie en su sano juicio quiere definir la implementación o plataforma para un binario. Por lo tanto, no puede tomar un dll de Windows x86 y comenzar a usarlo en una plataforma Linux x86_64. Eso sería un poco demasiado.

Sin embargo, lo que la gente quiere es lo mismo que tenemos con los módulos C: una interfaz estandarizada a nivel binario (es decir, una vez compilada). Actualmente, si desea cargar un archivo dll en una aplicación modular, exporta funciones C y las vincula en tiempo de ejecución. No puede hacer eso con un módulo C ++. Sería genial si pudieras, lo que también significaría que los archivos dlls escritos con un compilador podrían ser cargados por uno diferente. Claro, aún no podría cargar un archivo DLL creado para una plataforma incompatible, pero ese no es un problema que deba solucionarse.

Entonces, si el cuerpo de estándares definió qué interfaz expuso un módulo, entonces tendríamos mucha más flexibilidad para cargar módulos C ++, no tendríamos que exponer el código C ++ como código C, y probablemente tendríamos mucho más uso de C ++ en lenguajes de script.

Tampoco tendríamos que sufrir cosas como COM que intentan proporcionar una solución a este problema.

gbjbaanb
fuente
1
+1. Si estoy de acuerdo. Las otras respuestas aquí básicamente eliminan el problema al decir que la estandarización binaria prohibiría las optimizaciones específicas de la arquitectura. Pero ese no es el punto. Nadie está discutiendo por algún formato ejecutable binario multiplataforma. El problema es que no hay una interfaz estándar para cargar módulos C ++ dinámicamente.
Charles Salvia
1

Hay muchos problemas de portabilidad con C ++, que se debe solo a la falta de estandarización a nivel binario.

No creo que sea tan simple. Las respuestas proporcionadas ya proporcionan una excelente justificación en cuanto a la falta de enfoque en la estandarización, pero C ++ puede ser un lenguaje demasiado rico para ser adecuado para competir genuinamente con C como estándar ABI.

Podemos entrar en el cambio de nombre como resultado de la sobrecarga de funciones, incompatibilidades de vtable, incompatibilidades con excepciones a través de los límites del módulo, etc. Todo esto es un verdadero problema, y ​​desearía que al menos pudieran estandarizar los diseños de vtable.

Pero un estándar ABI no se trata solo de hacer discos duros C ++ producidos en un compilador capaces de ser utilizados por otro binario construido por un compilador diferente. ABI se usa en varios idiomas . Sería bueno si al menos pudieran cubrir la primera parte, pero no hay forma de que vea que C ++ compita realmente con C en el tipo de nivel ABI universal tan crucial para hacer los dibes más compatibles.

Imagine un simple par de funciones exportadas de esta manera:

void f(Foo foo);
void f(Bar bar, int val);

... e imagina Fooy Barfueron clases con constructores parametrizados, constructores de copia, constructores de movimiento y destructores no triviales.

Luego tome el escenario de Python / Lua / C # / Java / Haskell / etc. desarrollador tratando de importar este módulo y usarlo en su idioma.

Primero, necesitaríamos un estándar de cambio de nombre sobre cómo exportar símbolos utilizando la sobrecarga de funciones. Esta es una parte más fácil. Sin embargo, no debería ser realmente el nombre "destrozar". Dado que los usuarios de dylib tienen que buscar símbolos por nombre, las sobrecargas aquí deberían conducir a nombres que no parecen un desastre completo. Tal vez los nombres de los símbolos podrían ser "f_Foo" "f_Bar_int"o algo por el estilo. Tendríamos que asegurarnos de que no puedan entrar en conflicto con un nombre realmente definido por el desarrollador, tal vez reservando algunos símbolos / caracteres / convenciones para el uso de ABI.

Pero ahora es un escenario más difícil. ¿Cómo el desarrollador de Python, por ejemplo, invoca constructores de movimientos, constructores de copias y destructores? Tal vez podríamos exportarlos como parte del dylib. Pero, ¿ Fooy si Barse exportan en diferentes módulos? ¿Deberíamos duplicar los símbolos y las implementaciones asociadas en este dylib o no? Sugeriría que lo hagamos, ya que podría ser realmente molesto muy rápido, de lo contrario, comenzar a enredarse en múltiples interfaces dylib solo para crear un objeto aquí, pasarlo aquí, copiar uno allí, destruirlo aquí. Si bien la misma preocupación básica podría aplicarse de alguna manera en C (solo de forma más manual / explícita), C tiende a evitar esto solo por la forma en que las personas programan con él.

Esta es solo una pequeña muestra de la incomodidad. ¿Qué sucede cuando una de las ffunciones anteriores arroja unBazException (también una clase C ++ con constructores y destructores y derivando std :: excepción) en JavaScript?

En el mejor de los casos, creo que solo podemos esperar estandarizar un ABI que funcione desde un binario producido por un compilador de C ++ a otro binario producido por otro. Eso sería genial, por supuesto, pero solo quería señalar esto. Por lo general, acompañar tales inquietudes para distribuir una biblioteca generalizada que funcione con compiladores cruzados también suele ser el deseo de que sea un lenguaje cruzado realmente generalizado y compatible.

Solución sugerida

Mi solución sugerida después de luchar por encontrar formas de usar interfaces C ++ para API / ABI durante años con interfaces de estilo COM es convertirme en un desarrollador "C / C ++" (juego de palabras).

Use C para crear esas ABI universales, con C ++ para la implementación. Todavía podemos hacer cosas como exportar funciones que devuelven punteros a clases opacas de C ++ con funciones explícitas para crear y destruir tales objetos en el montón. Intente enamorarse de esa estética C desde una perspectiva ABI, incluso si estamos usando totalmente C ++ para la implementación. Las interfaces abstractas pueden modelarse utilizando tablas de punteros de función. Es tedioso incluir todo esto en una API C, pero los beneficios y la compatibilidad de la distribución que viene con eso tenderán a hacer que valga la pena.

Entonces, si no nos gusta usar esta interfaz directamente (probablemente no deberíamos hacerlo al menos por razones RAII), podemos envolver todo lo que queramos en una biblioteca C ++ estáticamente vinculada que enviamos con el SDK. Los clientes de C ++ pueden usar eso.

Los clientes de Python no querrán usar una interfaz C o C ++ directamente, ya que no hay formas de hacer esas pythonique. Querrán envolverlo en sus propias interfaces de Pythonique, por lo que en realidad es bueno que solo estemos exportando un mínimo de C API / ABI para que sea lo más fácil posible.

Creo que gran parte de la industria de C ++ se beneficiaría de hacer esto más que tratar de enviar tercamente interfaces de estilo COM, etc. También facilitaría todas nuestras vidas como usuarios de estos dylibs para no tener que preocuparse por las incómodas ABI. C lo hace simple, y su simplicidad desde una perspectiva ABI nos permite crear API / ABI que funcionan de forma natural y con minimalismo para todo tipo de FFI.


fuente
1
"Use C para crear esas ABI universales, con C ++ para la implementación". ... Yo hago lo mismo, como muchos otros!
Nawaz
-1

No sé por qué no se estandariza a nivel binario. Pero sé lo que hago al respecto. En Windows declaro la función extern "C" BOOL WINAPI. (Por supuesto, reemplace BOOL con cualquier tipo de función). Y se exportan limpiamente.

Mike Jones
fuente
2
Pero si lo declara extern "C", usará el C ABI, que es un estándar de facto en el hardware de PC común a pesar de que no es impuesto por ningún tipo de comité.
Billy ONeal
-3

Úselo unzip foo.zip && make foo.exe && foo.exesi desea la portabilidad de su fuente.

Sjoerd
fuente