- El código que sigue solo el estándar ISO C es poco común, y en la práctica las bases de código en C dependen de extensiones no estándar para añadir funciones y sortear huecos específicos de compiladores y bibliotecas
- Un compilador de C útil debe poder procesar primero encabezados del sistema como
<stdio.h>, pero glibc levanta barreras con extensiones GNU y supuestos como__attribute__((packed))y#include_next - La lógica de byteswapping de SDL puede elegir inline assembly cuando existen macros de ISA, así que incluso compiladores que no son GCC ni clang pueden verse obligados a soportar extensiones al estilo GCC
- El manejo de
extern inlineen OpenBSD y Gnulib complica la compatibilidad de la semántica de inline por las diferencias entre C99 y GCC, las bifurcaciones por plataforma y las condiciones de_FORTIFY_SOURCE - Los compiladores de C pequeños deben elegir entre parches upstream, parches downstream, conseguir guards dedicados o imitar la compatibilidad con GCC, y ampliar el uso de macros de prueba de funcionalidades parece una mejor dirección
La primera barrera que crean los headers de glibc
- Para ser un compilador de C útil, hay que poder preprocesar y parsear los headers de la biblioteca C del sistema, y si no puedes procesar
<stdio.h>, es difícil incluso pasar un hello world - En entornos GNU/Linux, esa barrera lleva a glibc
- glibc determina qué extensiones son compatibles revisando macros predefinidas por el compilador en sys/cdefs.h, archivo que casi todos los headers de libc terminan incluyendo indirectamente
- Las extensiones no compatibles se manejan quitando las definiciones relacionadas, pero esa lógica de compatibilidad también puede romperse en la práctica
-
struct epoll_eventy__attribute__((packed))struct epoll_eventensys/epoll.hde Linux es un packed struct que usa__attribute__((packed))de GNU- Ese atributo cambia el layout de la estructura en 64 bits, así que si se ignora, se rompe el ABI
- No basta con que el compilador implemente
__attribute__((packed)) - En
sys/cdefs.hhay código que define__attribute__(xyz)como una macro vacía si no es GCC, clang ni tcc - Como resultado, otros compiladores pueden perder ese atributo en los headers de glibc aunque sí soporten packed
- También se puede objetar que, como el header de
epolles específico de Linux, no es fácil aplicar sin más el criterio de portabilidad del estándar C
-
limits.hy#include_next- Algunos headers de C como
stddef.h,stdint.h,limits.hyfloat.htambién son necesarios en implementaciones freestanding, así que el compilador debe proveerlos - POSIX exige que
limits.hdefina también constantes específicas de POSIX además de las constantes estándar de C, por lo que hace falta unlimits.hespecífico de la plataforma por encima dellimits.hdel compilador - El
<limits.h>de glibc define directamente los valores ANSIlimits.hcuando no es GNU C, y en entornos GCC trae el header del compilador con#include_next <limits.h> - Esta estructura asume que el
limits.hbuiltin exclusivo de GCC define ciertas macros, y además depende de la extensión#include_next - clang también tiene que esquivar esta estructura
- Algunos headers de C como
La detección de capacidades en SDL y el problema de inline assembly
- Las funciones de byteswapping de
SDL_endian.husan builtins del compilador o inline assembly cuando es posible, y como último recurso recurren a una implementación normal con operaciones de bits - La lógica de detección funciona, a grandes rasgos, en este orden
- Si es GCC o clang y existe
__has_builtin(__builtin_bswapX), usa el builtin - Si es MSVC 8.0 o superior, usa el intrinsic
#pragmade MSVC - Si está definida alguna macro específica de ISA como
__x86_64__, usa inline assembly - En cualquier otro caso, usa la implementación normal con operaciones de bits
- Si es GCC o clang y existe
- Si un compilador que no es GCC ni clang define macros predefinidas por ISA por razones válidas, ese orden se vuelve problemático
- Incluso si ese compilador ofrece el builtin de bswap y el operador especial
__has_builtin, la lógica puede intentar usar inline assembly al estilo GCC - El resultado es una estructura que termina esperando que un compilador desconocido también soporte inline assembly estilo GCC
libc de OpenBSD y la confusión con extern inline
- Algunos headers de OpenBSD incluyen definiciones de funciones inline que el compilador puede usar opcionalmente durante la optimización
- Estas funciones se definen con la macro
__only_inline, y si el compilador no hace realmente inline, entonces debe sustituirlas por símbolos externos - Es decir, se necesitan funciones inline con extern linkage
-
Diferencias entre inline de C99 e inline de GCC
inlineestá especificado en C99, pero el comportamiento estándar entra en conflicto con el comportamiento no estándar de GCC anterior a C99- Para definiciones inline dentro de headers, hay que usar
extern inlinejunto con el cuerpo de la función, y en ese caso no se emite una función exportada real - En la translation unit, la declaración debe llevar solo
inlinepara exportar la definición de la función - El significado de
inlinetambién difiere entre C++ y C - Esta diferencia se explica con más detalle en el artículo de Youtao Guo
-
__only_inlinede OpenBSD- OpenBSD depende de la semántica de inline de GCC
- Para cubrir diferencias entre versiones de GCC, la macro
__only_inlineen sys/cdefs.h especifica explícitamente la antigua semánticagnu89 inlinemediante__attribute__en versiones modernas de GCC - En compiladores no GNU,
__only_inlinese define con linkagestatic - Como resultado, las funciones pueden declararse y definirse con linkage en conflicto, y romperse
-
El bypass
_ANSI_LIBRARY- OpenBSD respeta la macro
_ANSI_LIBRARY - Si se define esa macro, se omite por completo el uso de las definiciones problemáticas de
__only_inlineen headers estándar comosignal.h - No se obtiene la versión optimizada, pero al menos la compilación funciona
- OpenBSD respeta la macro
-
Código de compatibilidad de
extern inlineen Gnulib- El código de compatibilidad de
extern inlinede Gnulib también aparece al compilar Guile y nano - extern-inline.m4 contiene ramas condicionales complejas para manejar implementaciones rotas y extrañas de este corner case de C
- Entre las condiciones se reflejan diferencias de entorno como Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C,
_FORTIFY_SOURCE,__GNUC_STDC_INLINE__y__GNUC_GNU_INLINE__
- El código de compatibilidad de
La suposición de clang en Android bionic
- bionic es la libc de Android, y sus headers asumen clang aún más fuertemente que GCC
- Los headers de bionic usan mucho extensiones exclusivas de clang como
_Nonnully_Null_unspecifiedpara nullability checks - Estas macros no son difíciles de anular con
#definedesde la línea de comandos - Este problema se hace visible en los headers de bionic al usar un teléfono Android como entorno nativo de desarrollo aarch64 mediante Termux
_Null_unspecifiedtambién se conoce como__BIONIC_COMPLICATED_NULLNESS, y la definición relacionada está en sys/cdefs.h de bionic
Las opciones que enfrentan los compiladores de C pequeños
- El código que sigue solo el estándar ISO C es raro en la práctica, y muchas bases de código en C dependen de comportamientos no estándar y extensiones del lenguaje
- Esa dependencia surge no solo por funciones adicionales, sino también al sortear bugs y huecos distintos entre compiladores y bibliotecas
- Las bases de código que quieren soportar varios entornos dependen de chequeos y guards del preprocesador, pero este enfoque se rompe con facilidad y es difícil de manejar
- Al crear un compilador de C como antcc, estos problemas de compatibilidad aparecen una y otra vez
- Cuando muchos proyectos open source dependen de extensiones y comportamientos no estándar específicos del compilador incluso para cosas no esenciales, la carga de adaptación para compiladores alternativos aumenta
- Al mismo tiempo, tampoco es realista exigir que todos los desarrolladores prueben su código C en múltiples compiladores, incluyendo compiladores pequeños y poco conocidos
- La portabilidad en C ya es bastante difícil por sí sola
- Desde el punto de vista del autor de un compilador, hay cuatro opciones posibles
- Intentar corregir la incompatibilidad con parches upstream
- Volverse lo bastante conocido como para que los desarrolladores agreguen chequeos
#ifdefdedicados y pruebas básicas - Resolverlo downstream y distribuir parches o parches separados
- Fingir ser una versión concreta de GCC e implementar esas extensiones
- Los parches upstream parecen una batalla difícil de ganar, y los parches downstream son el camino más fácil
- Para soportar muchas bases de código con la menor confusión posible para usuarios y desarrolladores, imitar la compatibilidad con GCC es una opción realista, pero con una carga de implementación alta
- clang define
__GNUC__=4,__GNUC_MINOR__=2y__GNUC_PATCHLEVEL__=1para afirmar compatibilidad con GCC 4.2.1 - Aunque hoy clang es casi un objetivo de soporte separado, hizo falta un gran esfuerzo —incluyendo parches en ambos proyectos— para lograr que el kernel de Linux pudiera compilarse con clang
Las macros de GCC y el problema de ir alcanzando
- Fingir ser GCC también tiene problemas
- Muchas bases de código solo revisan
#ifdef __GNUC__y pueden usar extensiones recientes de GCC sin comprobar la versión - En ese caso, el compilador alternativo tiene que seguir poniéndose al día continuamente
- Una de las razones por las que clang no sube el valor de la macro
__GNUC__aunque soporte extensiones GNU más nuevas que 4.2.1 está aquí - El contexto relacionado aparece en el debate de LLVM sobre subir la versión menor de
__GNUC__en clang
Una mejor dirección y el estado actual
- Idealmente, en lugar de guards por compilador y chequeos de versión, deberían usarse más las macros de prueba de funcionalidades
- Entre las macros útiles de prueba de funcionalidades están
__has_builtin,__has_featurey__has_attribute - También podría usarse más el enfoque de macros estándar como
__STDC_NO_VLA__ - En el mundo *NIX actual, para bien o para mal, el estado base es un duopolio de facto GCC/clang
- También continúa el desarrollo de compiladores de C pequeños e independientes
1 comentarios
Comentarios en Lobste.rs
(autor del compilador kefir) El problema de
__attribute__en<sys/cdefs.h>está, por experiencia, entre los más molestos. Rompe epoll, las estructuraspackedcomunes, los constructores y la visibilidad de símbolos, así que terminé incluyendo este header de monkey patch junto con kefirNo es ideal, pero probablemente sea la forma más realista y, de hecho, nos permitió quitar la mayoría de los parches personalizados en suites de prueba externas
Otro tipo de fallo son los caminos alternativos con bugs. Algunos proyectos intentan detectar el compilador y ajustarse, pero como se prueban poco en compiladores alternativos, el código de fallback está lleno de errores o mal mantenido. Desde la perspectiva de quien hace un compilador, eso es mucho más irritante que fallar de inmediato como “compilador no soportado”. Por ejemplo, porque terminas depurando directamente miscompilaciones raras, como discrepancias en el ancho de
typedefenteros entre el programa y bibliotecas precompiladas$TERMcomoxterm-256colory finges ser xterm, se rompe de todoDe verdad no sé cómo resolverlo. Al final parece que nuestro proyecto solo tiene que volverse lo bastante difundido y famoso. ¡Facilísimo!
Creo que también me he topado varias veces con miscompilaciones raras causadas por fallbacks de detección de compilador mal mantenidos, y sí, son realmente molestas
Como desarrollo cproc sobre todo en linux-musl, no sabía que glibc desactiva
__attribute__en otros compiladores, pero en realidad es una situación bastante mala. El comentario dice que no pasa nada si se ignora el uso de attributes, pero no toma en cuenta que la mayoría del código de aplicaciones incluyesys/cdefs.hde forma indirecta y puede usar attributes que no deberían ignorarseAdemás de
packed,alignedyconstructortambién se usan muchoMe pregunto si esto está reportado en algún issue tracker. Parece que la mayoría de los usos de attributes dentro de
cdefs.hya están protegidos con__glibc_has_attribute, así que también me pregunto qué logra realmente desactivar__attribute__de forma global y si se podría eliminarTambién es un problema cuando los headers de libc usan funciones cuya compatibilidad el compilador no puede indicar bien. Son funciones que no se exponen de una manera como
__has_attributeo__has_builtin; un ejemplo que se me ocurre son las etiquetas__asm__. NetBSD las usa para renombrar símbolos y lanza#errorsi no existe__GNUC__o__PCC__. Aun así, no sé qué proponer aparte de simplemente dejar que se intente y falle si no hay soporteTambién tuve problemas relacionados con
__builtin_va_list. Hay casos en que libc, sin__GNUC__, defineva_listcomovoid *o incluso deja definiciones en conflicto. Esto tampoco puede probarse con__has_builtin. Puede que__has_builtin(__builtin_va_arg)sea una prueba suficientemente buena, pero no tengo claro cómo hacer que eso se corrija en macOS__attribute__en/usr/include/sysy/usr/include/bits, y había muchos usos sin protección. Principalmente__format__,__aligned__,__noreturn__, así que también habría que corregir esosEn general, glibc no parece priorizar la compatibilidad con compiladores que no sean GCC, así que no sé qué tan probable sea que acepten ese tipo de parches. A principios de este año, después de una actualización del sistema, glibc añadió un uso sin protección de
__SIZE_TYPE__en headers de Linux y eso hizo que mi compilador ya no pudiera compilar algunos proyectos. Lo reporté, pero todavía no lo arreglan, y al final terminé agregando macros predefinidos estilo__X_TYPE__para ajustarme a GCCNo se me ocurre una buena solución para el problema de las etiquetas
__asm__. Pero si cambiar nombres asm realmente es 100% necesario para que funcione, quizá sea mejor simplemente intentarlo y dejar que falle, en vez de hacer comprobaciones del compiladorLo de
__builtin_va_listes bastante serio. Yo esperaba que__has_builtin(__builtin_va_list)funcionara, pero al parecer no