- Código que segue apenas o padrão ISO C é raro, e codebases C reais dependem de extensões não padronizadas para adicionar funcionalidades e contornar lacunas específicas de compiladores e bibliotecas
- Um compilador C útil precisa ao menos processar cabeçalhos do sistema como
<stdio.h>, mas a glibc cria barreiras com extensões GNU e suposições como__attribute__((packed))e#include_next - A lógica de byteswapping da SDL pode escolher inline assembly quando há macros de ISA, o que pode acabar exigindo extensões no estilo GCC até de compiladores que não são GCC nem clang
- O tratamento de
extern inlineem OpenBSD e Gnulib torna a compatibilidade da semântica de inline complicada por causa das diferenças entre C99 e GCC, ramificações por plataforma e condições como_FORTIFY_SOURCE - Compiladores C pequenos precisam escolher entre patches upstream, patches downstream, obter guards dedicados ou imitar compatibilidade com GCC, e a expansão de macros de teste de recursos parece um caminho melhor
A primeira barreira criada pelos headers da glibc
- Para ser um compilador C útil, é preciso conseguir preprocessar e fazer parsing dos headers da biblioteca C do sistema, e se não conseguir processar
<stdio.h>, fica difícil até passar por um hello world - Em ambientes GNU/Linux, essa barreira leva à glibc
- A glibc verifica macros predefinidas pelo compilador em sys/cdefs.h, incluído indiretamente por quase todos os headers da libc, para determinar quais extensões são suportadas
- Extensões não suportadas são tratadas removendo definições relacionadas, mas essa lógica de compatibilidade também pode quebrar na prática
-
struct epoll_evente__attribute__((packed))- A
struct epoll_eventemsys/epoll.hno Linux é uma packed struct que usa GNU__attribute__((packed)) - Esse atributo altera o layout da struct em 64 bits, então ignorá-lo quebra a ABI
- Não basta que o compilador implemente
__attribute__((packed)) - Em
sys/cdefs.h, há código que define__attribute__(xyz)como uma macro vazia se não for GCC, clang ou tcc - Como resultado, outros compiladores podem ter esse atributo removido nos headers da glibc mesmo que suportem
packed - Também é possível argumentar que, como o header de
epollé específico de Linux, é difícil aplicar diretamente os critérios de portabilidade do padrão C
- A
-
limits.he#include_next- Alguns headers C, como
stddef.h,stdint.h,limits.hefloat.h, também são necessários em implementações freestanding, então precisam ser fornecidos pelo compilador - O POSIX exige que
limits.hdefina, além das constantes padrão de C, constantes específicas do POSIX, então é necessário umlimits.hda plataforma sobre olimits.hdo compilador - O
<limits.h>da glibc define diretamente valores ANSIlimits.hquando não está em GNU C e, em ambiente GCC, puxa o header do compilador com#include_next <limits.h> - Essa estrutura assume que o
limits.hbuiltin específico do GCC define certas macros e também depende da extensão#include_next - O clang também precisa contornar essa estrutura
- Alguns headers C, como
Detecção de recursos na SDL e o problema de inline assembly
- As funções de byteswapping em
SDL_endian.husam builtin do compilador ou inline assembly quando possível, e no último caso recorrem a uma implementação comum com operações de bits - A lógica de detecção funciona aproximadamente nesta ordem
- Se for GCC ou clang e houver
__has_builtin(__builtin_bswapX), usa builtin - Se for MSVC 8.0 ou superior, usa
#pragmaintrinsic do MSVC - Se macros específicas de ISA como
__x86_64__estiverem definidas, usa inline assembly - Caso contrário, usa a implementação comum com operações de bits
- Se for GCC ou clang e houver
- Se um compilador que não é GCC nem clang definir macros predefinidas específicas de ISA por um motivo razoável, essa ordem vira um problema
- Mesmo que esse compilador forneça builtin de bswap e o operador especial
__has_builtin, a lógica pode acabar tentando usar inline assembly no estilo GCC - Na prática, a estrutura passa a presumir que um compilador desconhecido também suporta inline assembly no estilo GCC
OpenBSD libc e a confusão de extern inline
- Alguns headers do OpenBSD incluem definições de funções inline que o compilador pode usar opcionalmente durante otimização
- Essas funções são definidas com a macro
__only_inline, e se o compilador não fizer inline de fato, elas precisam ser substituídas por símbolos externos - Ou seja, é necessária uma função inline com ligação externa
-
Diferenças entre inline em C99 e inline no GCC
inlineestá especificado no C99, mas o comportamento padrão entra em conflito com o comportamento não padronizado do GCC anterior ao C99- Definições inline dentro de headers precisam usar
extern inlinejunto com o corpo da função e, nesse caso, não emitem a função exportada real - Na translation unit, é preciso declarar a função apenas com
inlinepara exportar sua definição - O significado de
inlinetambém é diferente em C++ e em C - Essa diferença é tratada em detalhe no texto de Youtao Guo
-
__only_inlineno OpenBSD- O OpenBSD depende da semântica de inline do GCC
- Para encobrir diferenças entre versões do GCC, a macro
__only_inlineem sys/cdefs.h especifica explicitamente a antiga semântica gnu89 inline em GCCs recentes por meio de__attribute__ - Em compiladores não GNU,
__only_inlineé definido com linkagestatic - Como resultado, a função pode ser declarada e definida com linkages conflitantes, quebrando a compilação
-
O contorno
_ANSI_LIBRARY- O OpenBSD respeita a macro
_ANSI_LIBRARY - Se essa macro for definida, o uso das definições problemáticas de
__only_inlineé totalmente omitido em headers padrão comosignal.h - Você não obtém a versão otimizada, mas pelo menos o build funciona
- O OpenBSD respeita a macro
-
O código de compatibilidade de
extern inlineno Gnulib- O código de compatibilidade de
extern inlinedo Gnulib também aparece ao compilar Guile e nano - extern-inline.m4 contém uma lógica condicional complexa para lidar com implementações quebradas e estranhas desse caso extremo de C
- As condições refletem diferenças de ambiente envolvendo Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C,
_FORTIFY_SOURCE,__GNUC_STDC_INLINE__e__GNUC_GNU_INLINE__
- O código de compatibilidade de
As suposições de clang no bionic do Android
- O bionic é a libc do Android, e seus headers pressupõem clang de forma ainda mais forte que GCC
- Os headers do bionic usam bastante extensões específicas do clang, como
_Nonnulle_Null_unspecified, para nullability checks - Essas macros não são tão difíceis de neutralizar via
#definena linha de comando - Ao usar um celular Android como ambiente nativo de desenvolvimento aarch64 via Termux, esse problema aparece nos headers do bionic
_Null_unspecifiedtambém é chamado de__BIONIC_COMPLICATED_NULLNESS, e a definição relacionada está em sys/cdefs.h` do bionic
As escolhas que compiladores C pequenos enfrentam
- Código que segue apenas o padrão ISO C é raro na prática, e muitas codebases C dependem de comportamento não padronizado e de extensões da linguagem
- Essa dependência surge não só por funcionalidades extras, mas também ao contornar bugs e lacunas diferentes entre compiladores e bibliotecas
- Codebases que tentam suportar vários ambientes dependem de verificações e guards no preprocessador, mas essa abordagem quebra facilmente e é difícil de manter
- Ao criar um compilador C como o antcc, esses problemas de compatibilidade aparecem repetidamente
- Quando muitos projetos open source dependem de extensões e comportamentos não padronizados específicos de compilador mesmo para coisas não essenciais, a carga para compiladores alternativos aumenta
- Ao mesmo tempo, é difícil exigir que todo desenvolvedor teste código C em vários compiladores, incluindo os pequenos e menos conhecidos
- Portabilidade em C já é difícil por si só
- Do ponto de vista de quem escreve compiladores, há quatro caminhos possíveis
- Tentar corrigir a incompatibilidade com patches upstream
- Ficar conhecido o suficiente para que desenvolvedores adicionem verificações
#ifdefdedicadas e testes básicos por padrão - Resolver no downstream e distribuir patches ou patches separados
- Fingir ser uma versão específica do GCC e implementar essas extensões
- Patches upstream parecem uma batalha difícil de vencer, e patches downstream são o caminho mais fácil
- Para suportar muitas codebases com o mínimo de confusão para usuários e desenvolvedores, imitar compatibilidade com GCC é realista, mas traz um grande custo de implementação
- O clang define
__GNUC__=4,__GNUC_MINOR__=2e__GNUC_PATCHLEVEL__=1para alegar compatibilidade com GCC 4.2.1 - Hoje o clang é quase um alvo de suporte separado, mas foi necessário um grande esforço, com patches em ambos os projetos, só para viabilizar compilar o kernel Linux com clang
Macros GCC e o problema de correr atrás
- Fingir ser GCC também traz problemas
- Muitas codebases verificam apenas
#ifdef __GNUC__e podem usar extensões modernas do GCC sem checar a versão - Nesse caso, compiladores alternativos precisam continuar correndo atrás
- Esse é um dos motivos pelos quais o clang, apesar de suportar extensões GNU mais novas que 4.2.1, não aumenta o valor da macro
__GNUC__ - O contexto relacionado aparece na discussão do LLVM sobre elevar a minor version de
__GNUC__
Um caminho melhor e o estado atual
- Idealmente, macros de teste de recursos deveriam ser usadas de forma mais ampla no lugar de guards por compilador e checagens de versão
- Algumas macros úteis para isso são
__has_builtin,__has_featuree__has_attribute - Abordagens com macros padrão, como
__STDC_NO_VLA__, também poderiam ser mais comuns - No mundo *NIX atual, para o bem ou para o mal, o estado padrão é um quase duopólio GCC/clang
- O desenvolvimento de compiladores C pequenos e independentes continua
1 comentários
Comentários no Lobste.rs
(autor do compilador kefir) O problema de
__attribute__em<sys/cdefs.h>está, pela minha experiência, entre os mais dolorosos. Ele quebraepoll, as estruturaspackedcomuns, construtores e visibilidade de símbolos, então acabei incluindo este header com monkey patch junto com o kefirNão é o ideal, mas provavelmente é a abordagem mais realista e, na prática, isso permitiu remover a maior parte dos patches customizados na suíte de testes externa
Outro tipo de falha é código alternativo com bugs. Alguns projetos tentam detectar o compilador para se ajustar, mas como são pouco testados com compiladores alternativos, o código de fallback acaba cheio de bugs ou mal mantido. Do ponto de vista de quem escreve um compilador, isso é muito mais irritante do que falhar imediatamente com “compilador não suportado”. Por exemplo, porque você precisa depurar diretamente miscompilações estranhas, como incompatibilidades na largura de
typedefinteiros entre o programa e bibliotecas pré-compiladas$TERMcomoxterm-256colore fingir ser um xterm, um monte de coisa quebraRealmente não faço ideia de como resolver isso. No fim, será que nosso projeto só precisa ficar difundido e famoso o bastante? Fácil!
Acho que também já passei algumas vezes por essas miscompilações estranhas causadas por fallbacks de detecção de compilador mal mantidos, e é realmente irritante
Como desenvolvo o cproc principalmente em linux-musl, eu não sabia que a glibc desativa
__attribute__para outros compiladores, mas a situação é de fato bem ruim. Os comentários dizem que não tem problema ignorar o uso de attributes, mas isso não leva em conta que a maioria do código de aplicação incluisys/cdefs.hindiretamente e pode usar attributes que não devem ser ignoradosAlém de
packed,alignedeconstructortambém são bastante usadosFico curioso se isso foi reportado em algum issue tracker. Parece que a maior parte do uso de attributes dentro de
cdefs.hjá é protegida por__glibc_has_attribute, então também fico me perguntando o que essa desativação geral de__attribute__realmente pretende alcançar e se ela poderia ser removidaTambém é um problema quando headers da libc usam recursos para os quais o compilador não tem uma boa forma de indicar suporte. São recursos que não aparecem por mecanismos como
__has_attributeou__has_builtin; um exemplo que me vem à cabeça são os rótulos__asm__. O NetBSD os usa para renomear símbolos e emite#errorse não houver__GNUC__ou__PCC__. Mas, fora deixar tentar e falhar se não houver suporte, também não sei bem o que sugerirTambém já tive problemas relacionados a
__builtin_va_list. Há casos em que a libc, sem__GNUC__, defineva_listcomovoid *, ou até coloca definições conflitantes. Isso também não dá para testar com__has_builtin.__has_builtin(__builtin_va_arg)até pode ser um teste bom o bastante, mas não faço ideia de como convencer o macOS a corrigir isso__attribute__em/usr/include/syse/usr/include/bits, encontrei muitos usos sem proteção. Em geral eram__format__,__aligned__e__noreturn__, então isso também precisaria ser corrigidoNo geral, a glibc não parece tratar compatibilidade com compiladores que não sejam GCC como prioridade, então não sei qual seria a chance de aceitarem esse tipo de patch. No começo deste ano, depois de uma atualização do sistema, a glibc adicionou aos headers do Linux um uso não protegido de
__SIZE_TYPE__, e isso fez meu compilador deixar de compilar alguns projetos. Eu reportei, mas ainda não foi corrigido, e no fim acabei adicionando macros predefinidas no estilo__X_TYPE__para ficar compatível com o GCCQuanto ao problema dos rótulos
__asm__, não me ocorre uma solução muito boa. Mas, se a renomeação de nomes em asm for realmente 100% necessária para o funcionamento, talvez seja melhor simplesmente tentar e deixar falhar do que fazer uma checagem de compilador__builtin_va_listé bem sério. Eu esperava que__has_builtin(__builtin_va_list)funcionasse, mas aparentemente não funciona