1 puntos por GN⁺ 17 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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 inline en 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_event y __attribute__((packed))

    • struct epoll_event en sys/epoll.h de 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.h hay 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 epoll es específico de Linux, no es fácil aplicar sin más el criterio de portabilidad del estándar C
  • limits.h y #include_next

    • Algunos headers de C como stddef.h, stdint.h, limits.h y float.h también son necesarios en implementaciones freestanding, así que el compilador debe proveerlos
    • POSIX exige que limits.h defina también constantes específicas de POSIX además de las constantes estándar de C, por lo que hace falta un limits.h específico de la plataforma por encima del limits.h del compilador
    • El <limits.h> de glibc define directamente los valores ANSI limits.h cuando no es GNU C, y en entornos GCC trae el header del compilador con #include_next <limits.h>
    • Esta estructura asume que el limits.h builtin exclusivo de GCC define ciertas macros, y además depende de la extensión #include_next
    • clang también tiene que esquivar esta estructura

La detección de capacidades en SDL y el problema de inline assembly

  • Las funciones de byteswapping de SDL_endian.h usan 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 #pragma de 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 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

    • inline está 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 inline junto 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 inline para exportar la definición de la función
    • El significado de inline también difiere entre C++ y C
    • Esta diferencia se explica con más detalle en el artículo de Youtao Guo
  • __only_inline de OpenBSD

    • OpenBSD depende de la semántica de inline de GCC
    • Para cubrir diferencias entre versiones de GCC, la macro __only_inline en sys/cdefs.h especifica explícitamente la antigua semántica gnu89 inline mediante __attribute__ en versiones modernas de GCC
    • En compiladores no GNU, __only_inline se define con linkage static
    • 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_inline en headers estándar como signal.h
    • No se obtiene la versión optimizada, pero al menos la compilación funciona
  • Código de compatibilidad de extern inline en Gnulib

    • El código de compatibilidad de extern inline de 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__

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 _Nonnull y _Null_unspecified para nullability checks
  • Estas macros no son difíciles de anular con #define desde 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_unspecified tambié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 #ifdef dedicados 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__=2 y __GNUC_PATCHLEVEL__=1 para 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_feature y __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 estructuras packed comunes, los constructores y la visibilidad de símbolos, así que terminé incluyendo este header de monkey patch junto con kefir
    No 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 typedef enteros entre el programa y bibliotecas precompiladas

    • Algo parecido pasa con las terminales. Si no pones $TERM como xterm-256color y finges ser xterm, se rompe de todo
      De verdad no sé cómo resolverlo. Al final parece que nuestro proyecto solo tiene que volverse lo bastante difundido y famoso. ¡Facilísimo!
    • El enfoque del header de monkey patch parece ser también el que usa slimcc, y parece una concesión bastante razonable
      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 incluye sys/cdefs.h de forma indirecta y puede usar attributes que no deberían ignorarse
    Además de packed, aligned y constructor también se usan mucho
    Me pregunto si esto está reportado en algún issue tracker. Parece que la mayoría de los usos de attributes dentro de cdefs.h ya 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 eliminar
    Tambié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_attribute o __has_builtin; un ejemplo que se me ocurre son las etiquetas __asm__. NetBSD las usa para renombrar símbolos y lanza #error si no existe __GNUC__ o __PCC__. Aun así, no sé qué proponer aparte de simplemente dejar que se intente y falle si no hay soporte
    También tuve problemas relacionados con __builtin_va_list. Hay casos en que libc, sin __GNUC__, define va_list como void * 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

    • Revisé rápidamente el uso de __attribute__ en /usr/include/sys y /usr/include/bits, y había muchos usos sin protección. Principalmente __format__, __aligned__, __noreturn__, así que también habría que corregir esos
      En 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 GCC
      No 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 compilador
      Lo de __builtin_va_list es bastante serio. Yo esperaba que __has_builtin(__builtin_va_list) funcionara, pero al parecer no