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?
Respostas:
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.
fonte
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 ++":
fonte
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
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 .
fonte
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
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.
fonte
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.
fonte
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:
... e imaginamos
Foo
eBar
eram 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
Foo
e se eBar
for 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
f
funções acima lança umBazException
(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
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.
fonte
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ê.Use
unzip foo.zip && make foo.exe && foo.exe
se você quiser portabilidade da sua fonte.fonte