C ++: Falta de padronização no nível binário

14

Por que ISO / ANSI não padronizou C ++ no nível binário? Existem muitos problemas de portabilidade no C ++, o que ocorre apenas por falta de padronização no nível binário.

Don Box escreve (citando seu livro Essential COM , capítulo COM As A Better C ++ )

C ++ e portabilidade


Depois que é tomada a decisão de distribuir uma classe C ++ como uma DLL, uma delas é confrontada com uma das fraquezas fundamentais do C ++ , ou seja, falta de padronização no nível binário . Embora o rascunho do documento de trabalho ISO / ANSI C ++ tente codificar quais programas serão compilados e quais serão os efeitos semânticos de sua execução, ele não tenta padronizar o modelo de tempo de execução binário do C ++. A primeira vez que esse problema se tornará evidente é quando um cliente tenta se vincular à biblioteca de importação da DLL do FastString de um ambiente de desenvolvimento em C ++ diferente daquele usado para criar a DLL do FastString.

Há mais benefícios ou perda dessa falta de padronização binária?

Nawaz
fonte
Isso é melhor solicitado em programmers.stackexchange.com , considerando que é mais uma pergunta subjetiva?
Stephen Furlani
1
Pergunta relacionada na verdade: stackoverflow.com/questions/2083060/…
AraK
4
Don Box é um fanático. Ignore-o.
John Dibling
8
Bem, C também não é padronizado por ANSI / ISO no nível binário; OTOH C possui um ABI padrão de fato , e não de jure . O C ++ não possui uma ABI tão padronizada porque fabricantes diferentes tinham objetivos diferentes com suas implementações. Por exemplo, exceções no VC ++ piggyback sobre o Windows SEH. O POSIX não possui SEH e, portanto, aceitar esse modelo não faria sentido (portanto, o G ++ e o MinGW não usam esse modelo).
Billy ONeal
3
Eu vejo isso como um recurso, não uma fraqueza. Se você vincular uma implementação a uma ABI específica, nunca teremos inovação e o novo hardware estará vinculado ao design da linguagem (e, já que há 15 anos entre cada nova versão que há muito tempo na indústria de hardware) e sufocando inovar novas idéias para executar o código com mais eficiência não será feita. O preço é que todo o código em um executável deve ser criado pelo mesmo compilador / versão (um problema, mas não o principal).

Respostas:

16

Os idiomas com o formato compilado compatível com binário são uma fase relativamente nova [*], por exemplo, os tempos de execução da JVM e .NET. Os compiladores C e C ++ geralmente emitem código nativo.

A vantagem é que não há necessidade de um JIT, um interpretador de bytecode, uma VM ou qualquer outra coisa. Por exemplo, você não pode escrever o código de inicialização que é executado na inicialização da máquina como um bytecode Java portátil e agradável, a menos que talvez a máquina possa executar nativamente o bytecode Java ou se você tem algum tipo de conversor de Java para um nativo não compatível com binário código executável (em teoria: não tenho certeza se isso pode ser recomendado na prática para o código de inicialização). Você pode escrevê-lo em C ++, mais ou menos, embora não seja portátil, mesmo no nível da fonte, pois ele fará muita bagunça com endereços mágicos de hardware.

A desvantagem é que, naturalmente, o código nativo é executado apenas na arquitetura para a qual foi compilado, e os executáveis ​​podem ser carregados apenas por um carregador que entende seu formato executável, e somente vinculam e chamam outros executáveis ​​para a mesma arquitetura e ABI.

Mesmo que você chegue tão longe, vincular dois executáveis ​​juntos só funcionará corretamente, desde que: (a) você não viole a regra de definição única, o que é fácil de fazer se eles foram compilados com diferentes compiladores / opções / o que for, de modo que eles usavam definições diferentes da mesma classe (em um cabeçalho ou porque cada um deles estava estaticamente vinculado a diferentes implementações); e (b) todos os detalhes relevantes da implementação, como o layout da estrutura, são idênticos de acordo com as opções do compilador em vigor quando cada um foi compilado.

Para o padrão C ++, definir tudo isso removeria muitas das liberdades atualmente disponíveis para os implementadores. Os implementadores estão usando essas liberdades, especialmente ao escrever código de nível muito baixo em C ++ (e C, que tem o mesmo problema).

Se você deseja escrever algo parecido com C ++, para um destino binário-portátil, há C ++ / CLI, que tem como alvo .NET e Mono, para que você possa (espero) executar o .NET em outro lugar que não o Windows. Eu acho que é possível persuadir o compilador da MS a produzir assemblies CIL puros que serão executados no Mono.

Também há potencialmente coisas que podem ser feitas, por exemplo, LLVM para criar um ambiente C ou C ++ binário-portátil. Porém, não sei se surgiu algum exemplo amplo.

Mas tudo isso depende da correção de muitas coisas que o C ++ torna dependentes da implementação (como os tamanhos dos tipos). Em seguida, o ambiente que compreende os binários portáteis deve estar disponível no sistema em que o código deve ser executado. Ao permitir binários não portáteis, C e C ++ podem ir a lugares onde os binários portáteis não podem, e é por isso que o padrão não diz nada sobre binários.

Então, em qualquer plataforma, as implementações geralmente ainda não fornecem compatibilidade binária entre diferentes conjuntos de opções, embora o padrão não as pare. Se Don Box não gostar que os compiladores da Microsoft possam produzir binários incompatíveis a partir da mesma fonte, de acordo com as opções do compilador, então é da equipe do compilador que ele precisa se queixar. A linguagem C ++ não proíbe que um compilador ou um sistema operacional aponte todos os detalhes necessários; portanto, uma vez que você se limita ao Windows, não é um problema fundamental com o C ++. A Microsoft optou por não fazer isso.

As diferenças geralmente se manifestam como mais uma coisa que você pode errar e travar seu programa, mas pode haver ganhos consideráveis ​​em eficiência entre, por exemplo, versões incompatíveis de depuração vs versão de uma dll.

[*] Não sei ao certo quando a idéia foi inventada, provavelmente em 1642 ou algo assim, mas sua popularidade atual é relativamente nova, em comparação com o tempo em que o C ++ se comprometeu com as decisões de design que impedem a definição da portabilidade binária.

Steve Jessop
fonte
@ Steve Mas C tem uma ABI bem definida no i386 e no AMD64, para que eu possa passar um ponteiro para uma função compilada pelo GCC versão X para uma função compilada pelo MSVC versão Y. Fazer isso com uma função C ++ é impossível.
user877329
7

Compatibilidade entre plataformas e compiladores não eram os principais objetivos por trás de C e C ++. Eles nasceram em uma época e destinavam-se a propósitos para os quais as minimizações de tempo e espaço específicas da plataforma e específicas do compilador eram cruciais.

De Stroustrup, "O design e a evolução do C ++":

"O objetivo explícito era combinar C em termos de tempo de execução, compactação de código e compactação de dados. ... O ideal - o que foi alcançado - era que C com Classes pudesse ser usado para qualquer que fosse C."

Andy Thomas
fonte
1
+1 - exatamente. Como alguém construiria uma ABI padrão que funcionasse nas caixas ARM e Intel? Não faria sentido!
quer
1
infelizmente, falhou nisso. Você pode fazer tudo o que C faz ... exceto carregar dinamicamente um módulo C ++ em tempo de execução. você precisa 'reverter' o uso das funções C na interface exposta.
precisa
6

Isto não é um erro, é um recurso! Isso dá aos implementadores liberdade para otimizar sua implementação no nível binário. O little-endian i386 e seus filhos não são os únicos CPUs que possuem ou existem.


fonte
6

O problema descrito na citação é causado pela evitação deliberada da padronização dos esquemas de manipulação de nomes de símbolos (acho que " padronização no nível binário " é uma frase enganosa a esse respeito, embora o problema esteja relacionado à Application Binary Interface de um compilador ( ABI).

O C ++ codifica as informações de assinatura e tipo de uma função ou objeto de dados, e sua associação de classe / namespace ao nome do símbolo, e diferentes compiladores podem usar esquemas diferentes. Conseqüentemente, um símbolo em uma biblioteca estática, DLL ou arquivo de objeto não será vinculado ao código compilado usando um compilador diferente (ou possivelmente até mesmo uma versão diferente do mesmo compilador).

O problema é descrito e explicado provavelmente melhor do que aqui , com exemplos de esquemas usados ​​por diferentes compiladores.

As razões para a falta deliberada de padronização também são explicadas aqui .

Clifford
fonte
3

O objetivo do ISO / ANSI era padronizar a linguagem C ++, questão que parece ser complexa o suficiente para exigir anos para ter uma atualização dos padrões de linguagem e suporte do compilador.

A compatibilidade binária é muito mais complexa, uma vez que os binários precisam ser executados em arquiteturas de CPUs diferentes e em diferentes ambientes de SO.


fonte
É verdade, mas o problema descrito na citação não tem nada a ver com "compatibilidade de nível binário" (apesar do uso do termo pelo autor), em qualquer sentido que não seja esse tipo de coisa, é definido em algo chamado "Interface Binária do Aplicativo". Na verdade, ele está descrevendo a questão de esquemas incompatíveis de separação de nomes.
@ Clifford: o esquema de manipulação de nomes é apenas um subconjunto da compatibilidade de nível binário. o último é mais como um termo genérico!
Nawaz
Duvido que exista um problema ao tentar executar um binário Linux em uma máquina Windows. As coisas seriam muito melhores se houvesse uma ABI por plataforma, pois pelo menos uma linguagem de script poderia carregar dinamicamente e executar um binário na mesma plataforma, ou os aplicativos poderiam usar componentes criados com um compilador diferente. Hoje você não pode usar uma DLL de C no linux, e ninguém reclama, mas essa DLL ainda pode ser carregada por um aplicativo python que é onde o benefício se acumula.
precisa
2

Como Andy disse, a compatibilidade entre plataformas não era um grande objetivo, enquanto a ampla implementação de plataforma e hardware era um objetivo, com o resultado líquido de que você pode escrever implementações conformes para uma seleção muito ampla de sistemas. A padronização binária tornaria isso praticamente inatingível.

A compatibilidade C também era importante e teria complicado significativamente isso.

Subseqüentemente, houve alguns esforços para padronizar a ABI para um subconjunto de implementações.

Flexo
fonte
Porra, eu esqueci a compatibilidade C. Bom ponto, +1!
Andy Thomas
1

Penso que a falta de um padrão para C ++ é um problema no mundo atual da programação modular desacoplada. No entanto, temos que definir o que queremos desse padrão.

Ninguém em sã consciência deseja definir a implementação ou plataforma para um binário. Portanto, você não pode pegar uma DLL do Windows x86 e começar a usá-la em uma plataforma Linux x86_64. Isso seria um pouco demais.

No entanto, o que as pessoas querem é a mesma coisa que temos nos módulos C - uma interface padronizada no nível binário (isto é, uma vez compilado). Atualmente, se você deseja carregar uma dll em um aplicativo modular, exporta as funções C e as vincula a elas em tempo de execução. Você não pode fazer isso com um módulo C ++. Seria ótimo se você pudesse, o que também significaria que dlls escritas com um compilador poderiam ser carregadas por um diferente. Claro, você ainda não seria capaz de carregar uma dll criada para uma plataforma incompatível, mas isso não é um problema que precise ser corrigido.

Portanto, se o corpo dos padrões definisse o que a interface expunha um módulo, teríamos muito mais flexibilidade no carregamento de módulos C ++, não precisaríamos expor o código C ++ como código C e provavelmente teríamos muito mais uso de C ++ em linguagens de script.

Também não precisaríamos sofrer coisas como COM que tentam fornecer uma solução para esse problema.

gbjbaanb
fonte
1
+1. Sim, eu concordo. As outras respostas aqui basicamente eliminam o problema, dizendo que a padronização binária proibiria otimizações específicas da arquitetura. Mas esse não é o ponto. Ninguém está discutindo por algum formato executável binário de plataforma cruzada. O problema é que não há interface padrão para carregar módulos C ++ dinamicamente.
Charles Salvia
1

Existem muitos problemas de portabilidade no C ++, o que ocorre apenas por falta de padronização no nível binário.

Eu não acho tão simples assim. As respostas fornecidas já fornecem uma excelente justificativa para a falta de foco na padronização, mas o C ++ pode ser uma linguagem muito rica para ser adequada para competir genuinamente com o C como um padrão ABI.

Podemos entrar na confusão de nomes resultante da sobrecarga de funções, incompatibilidades de vtable, incompatibilidades com exceções que ultrapassam os limites do módulo, etc. Tudo isso é uma verdadeira dor de cabeça, e eu gostaria que eles pudessem ao menos padronizar os layouts de vtable.

Mas um padrão ABI não se resume apenas a produzir dylibs C ++ produzidos em um compilador capazes de serem usados ​​por outro binário criado por um compilador diferente. A ABI é usada em vários idiomas . Seria bom se eles pudessem, pelo menos, cobrir a primeira parte, mas não há como eu ver o C ++ realmente competindo com C no tipo de nível ABI universal tão crucial para criar os dylibs mais amplamente compatíveis.

Imagine um simples par de funções exportadas assim:

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

... e imaginamos Fooe Bareram classes com construtores parametrizados, copiadores, movemos construtores e destruidores não triviais.

Depois, pegue o cenário de um Python / Lua / C # / Java / Haskell / etc. desenvolvedor tentando importar este módulo e usá-lo em seu idioma.

Primeiro, precisaríamos de um padrão de desconfiguração de nomes para exportar símbolos utilizando sobrecarga de funções. Esta é uma parte mais fácil. No entanto, não deveria realmente ser o nome "desconcertante". Como os usuários do dylib precisam procurar símbolos pelo nome, as sobrecargas aqui devem levar a nomes que não parecem uma bagunça completa. Talvez os nomes dos símbolos possam ser como "f_Foo" "f_Bar_int"ou algo desse tipo. Teríamos que ter certeza de que eles não podem colidir com um nome realmente definido pelo desenvolvedor, talvez reservando alguns símbolos / caracteres / convenções para o uso da ABI.

Mas agora um cenário mais difícil. Como o desenvolvedor do Python, por exemplo, invoca os construtores de movimentação, construtores de cópia e destruidores? Talvez possamos exportá-los como parte do dylib. Mas Fooe se e Barfor exportado em diferentes módulos? Devemos duplicar os símbolos e implementações associados a este dylib ou não? Eu sugiro que sim, já que pode ser realmente irritante muito rápido, caso contrário, comece a ter que se envolver em várias interfaces dylib apenas para criar um objeto aqui, passá-lo aqui, copiá-lo aqui, copiar um ali, destruí-lo aqui. Embora a mesma preocupação básica possa ser aplicada em C (apenas mais manualmente / explicitamente), C tende a evitar isso apenas por natureza, da maneira como as pessoas programam com ele.

Esta é apenas uma pequena amostra do constrangimento. O que acontece quando uma das ffunções acima lança um BazException(também uma classe C ++ com construtores e destruidores e derivando std :: exception) no JavaScript?

Na melhor das hipóteses, acho que podemos apenas padronizar uma ABI que funcione de um binário produzido por um compilador C ++ para outro binário produzido por outro. Isso seria ótimo, é claro, mas eu só queria salientar isso. Normalmente, acompanhar essas preocupações para distribuir uma biblioteca generalizada que funcione com compiladores cruzados também costuma ser o desejo de torná-la realmente compatível com linguagens cruzadas generalizadas e compatíveis.

Solução sugerida

Minha solução sugerida, depois de tentar encontrar maneiras de usar interfaces C ++ para APIs / ABIs por anos com interfaces no estilo COM, é apenas me tornar um desenvolvedor "C / C ++" (trocadilho).

Use C para criar essas ABIs universais, com C ++ para a implementação. Ainda podemos fazer coisas como funções de exportação que retornam ponteiros para classes C ++ opacas com funções explícitas para criar e destruir esses objetos no heap. Tente se apaixonar por essa estética C de uma perspectiva ABI, mesmo se estivermos totalmente usando C ++ para a implementação. Interfaces abstratas podem ser modeladas usando tabelas de ponteiros de função. É tedioso agrupar essas coisas em uma API C, mas os benefícios e a compatibilidade da distribuição que vem com isso tendem a fazer com que valha a pena.

Então, se não gostamos de usar essa interface diretamente (provavelmente não devemos pelo menos por razões de RAII), podemos agrupar tudo o que queremos em uma biblioteca C ++ vinculada estaticamente que enviamos com o SDK. Clientes C ++ podem usar isso.

Os clientes Python não querem usar a interface C ou C ++ diretamente, pois não há maneiras de torná-las pythonicas. Eles querem agrupá-lo em suas próprias interfaces pythonique, por isso é bom que apenas exportemos uma API / ABI C mínima para facilitar o máximo possível.

Acho que grande parte da indústria de C ++ se beneficiaria disso mais do que tentar enviar de maneira teimosa interfaces de estilo COM e assim por diante. Isso também tornaria nossa vida mais fácil, pois os usuários desses dylibs não precisariam se preocupar com ABIs desajeitadas. C simplifica, e a simplicidade do ponto de vista ABI nos permite criar APIs / ABIs que funcionam naturalmente e com minimalismo para todos os tipos de IFIs.


fonte
1
"Use C para criar essas ABIs universais, com C ++ para a implementação." ... Eu faço o mesmo, como muitos outros!
Nawaz
-1

Não sei por que não é padronizado em nível binário. Mas eu sei o que faço sobre isso. No Windows, declaro a função externa "C" BOOL WINAPI. (É claro que substitua o BOOL por qualquer tipo de função.) E eles são exportados de maneira limpa.

Mike Jones
fonte
2
Mas se você o declarar extern "C", ele usará o C ABI, que é um padrão de fato no hardware de PC comum, mesmo que não seja imposto por nenhum tipo de comitê.
Billy ONeal
-3

Use unzip foo.zip && make foo.exe && foo.exese você quiser portabilidade da sua fonte.

Sjoerd
fonte