The Illusion of Portability: C Extensions and the Compiler Duopoly
For anyone who has spent significant time writing C, the ISO C standard is often viewed more as a theoretical ideal than a practical reality. In the real world, C code is rarely fully standard-adhering; instead, it is a patchwork of non-standard behaviors, compiler-specific extensions, and fragile preprocessor guards designed to navigate the gaps between different toolchains.
This tension becomes most apparent when building a new C compiler. To be useful, a compiler must be able to parse system headers—specifically glibc on Linux—which are often riddled with assumptions that only a handful of compilers can satisfy. When a codebase relies on a specific GNU extension to maintain its Application Binary Interface (ABI), a "standard" compiler that ignores that extension isn't just non-compliant; it's broken.
The Gauntlet of System Headers
The first major obstacle for any aspiring C compiler is the system's C library headers. On GNU/Linux, glibc attempts to maintain compatibility for non-GCC compilers, but these attempts are often incomplete or flawed.
The __attribute__ Trap
One common point of failure is the use of __attribute__((packed)). In sys/epoll.h, the struct epoll_event is a packed struct. Because packing changes the memory layout of the struct on 64-bit systems, ignoring the attribute breaks the ABI. However, glibc uses a preprocessor check in sys/cdefs.h that effectively disables the __attribute__ macro for any compiler that isn't GCC, Clang, or TCC:
#if !(defined __GNUC__ || defined __clang__ || defined __TINYC__)
# define __attribute__(xyz) /* Ignore */
#endif
If you are developing a new compiler, even if you implement __attribute__((packed)), the headers will simply strip it away unless you also pretend to be one of the "approved" compilers.
The limits.h Complexity
Another example is <limits.h>, which often requires a combination of compiler-internal definitions and platform-specific constants. glibc's implementation relies on the #include_next extension to pull in the compiler's own limits.h before adding POSIX-specific constants. This creates a dependency chain where the compiler must not only support the extension but also provide the expected internal headers in a specific directory structure.
Beyond glibc: SDL, OpenBSD, and Bionic
The struggle for portability extends beyond the primary C library into widely used libraries and other operating systems.
- SDL: The
SDL_endian.hheader uses a tiered detection system for byteswapping. If a compiler isn't GCC or Clang, it may fall back to extended inline assembly based on ISA macros (like__x86_64__), even if the compiler provides the necessary built-ins. This assumes that any compiler targeting x86_64 must also support GCC-style inline assembly. - OpenBSD: Some OpenBSD headers use a macro called
__only_inlineto provide optimized function definitions. These rely on GCC's specific inline semantics. On non-GNU compilers, these are often defined asstatic, which can lead to conflicting linkage errors. While the_ANSI_LIBRARYmacro can disable these, it removes the optimization benefits. - Bionic (Android): In a reversal of the Linux trend, Android's
bioniclibc headers heavily assume the use of Clang, utilizing Clang-specific nullability extensions like_Nonnulland_Null_unspecified.
The Compiler Developer's Dilemma
For those building independent compilers, the path to compatibility is fraught with difficulty. As noted by the developer of slimcc in the community discussion, the challenges are endless: from math.h's NAN printing differences to NetBSD headers that #error out if the compiler doesn't identify as GCC or PCC.
When faced with these incompatibilities, compiler authors generally have four options:
- Upstream Patches: Attempting to fix the headers in the OS or library source. This is often a losing battle due to the sheer scale of the projects.
- Popularity: Becoming popular enough that library authors explicitly add
#ifdefchecks for the new compiler. - Downstream Patches: Distributing patches for users to apply to the libraries they wish to compile.
- Impersonation: Defining
__GNUC__and implementing the corresponding extensions.
Impersonation is the most realistic path to broad compatibility. Clang, for instance, defines __GNUC__=4 to claim compatibility with GCC 4.2.1. This allows it to work with a vast array of legacy codebases without forcing every developer to rewrite their headers. However, this creates a "catch-up" game where the compiler must implement every extension that a user might expect based on the version number it claims to be.
Toward a Better Standard
The current state of C portability is a result of the GCC/Clang quasi-duopoly. While some argue that standard C99 or C11 is sufficient for embedded systems or non-PC platforms, the "real world" of *NIX development is deeply entwined with GNU extensions.
Ideally, the industry would move toward feature-test macros—such as __has_builtin, __has_attribute, and __has_feature—rather than relying on compiler-specific guards. This would allow compilers to signal their capabilities regardless of their identity. Until then, the burden of portability remains a complex dance of preprocessor hacks and strategic impersonation.