On C extensions, portability, and alternative compilers
lemon's site
On C extensions, portability, and alternative compilers
2026 May 24
compilers
Anyone who's written C knows that full ISO C standard-adhering code is an<br>impractical rarity. Most real world C code out there relies on non-standard<br>behaviors and language extensions to varying extents, and a lot of this isn't<br>for extra features, but just to work around bugs and gaps in different<br>compilers and libraries. A lot of codebases will try somewhat to support<br>various environments, mostly through the use of preprocessor checks and guards,<br>but these attempts are finicky at best and straight up broken at worst.
I have ran into many of these situations while working on my C compiler, so<br>here's a small list of some of them.
glibc
The system's C library headers is the first 'obstacle' for a C compiler<br>aspiring to be useful. If you can't preprocess and parse , you<br>won't get past hello world. Because I use GNU/Linux, that means glibc. Now, to<br>their credit, glibc does try to retain compatibility of its headers on non-GCC<br>compilers. In the monstrosity that is sys/cdefs.h, which is indirectly<br>included by every libc header, they use all kinds of preprocessor checks for<br>compiler-predefined macros to determine what kinds of compiler extensions are<br>supported, and #define things away when they aren't.
Unfortunately this is just broken sometimes. For example, on Linux struct epoll_event from sys/epoll.h is a packed<br>struct,<br>which uses the GNU __attribute__((packed)). Because this changes the struct<br>layout (on 64 bits), you can't ignore it without breaking ABI. So okay, say you implement<br>support for __attribute__((packed)) in your compiler. But this isn't enough,<br>because the aforementioned sys/cdefs.h contains this code:
/* GCC, clang, and compatible compilers have various useful declarations<br>that can be made with the '__attribute__' syntax. All of the ways we use<br>this do fine if they are omitted for compilers that don't understand it. */<br>#if !(defined __GNUC__ || defined __clang__ || defined __TINYC__)<br># define __attribute__(xyz) /* Ignore */<br>#endif
If you aren't gcc, clang, or tcc, tough luck.
Although, the epoll header is Linux-specific, so you could argue<br>that applying C standards portability criteria is not fair.
Some C headers are supposed to be provided by the compiler because they should<br>be present even on freestanding implementations, and rely on<br>compiler-internal definitions. In my computer for example, these live in<br>/usr/lib/gcc/x86_64-pc-linux-gnu/16.1.1/include/ for GCC and<br>/usr/lib/clang/22/include/ for clang. These builtin headers include<br>stddef.h, stdint.h, limits.h, float.h and more. However, POSIX requires<br>limits.h to define some POSIX-specific constants in addition to the standard<br>C constants. So you still need a platform-specific limits.h on top of the compiler's.
glibc's looks like this (abridged):
...
/* If we are not using GNU CC we have to define all the symbols ourself.<br>Otherwise use gcc's definitions (see below). */<br>#if !defined __GNUC__ || __GNUC__ for standard 32-bit words. */<br>/* These assume 8-bit `char's, 16-bit `short int's, and 32-bit `int's and `long int's. */<br># define CHAR_BIT 8<br>...<br># endif /* limits.h */<br>#endif /* GCC 2. */
#endif /* !_LIBC_LIMITS_H_ */<br>/* Get the compiler's limits.h, which defines almost all the ISO constants.
We put this #include_next outside the double inclusion check because<br>it should be possible to include this file more than once and still get<br>the definitions from gcc's header. */<br>#if defined __GNUC__ && !defined _GCC_LIMITS_H_<br>/* `_GCC_LIMITS_H_' is what GCC's file defines. */<br># include_next<br>#endif<br>/* The files in some gcc versions don't define LLONG_MIN, LLONG_MAX,<br>and ULLONG_MAX. */<br>#if defined __USE_ISOC99 && defined __GNUC__<br># ifndef LLONG_MIN<br># define LLONG_MIN (-LLONG_MAX-1)<br># endif<br>...<br>#endif
#ifdef __USE_POSIX<br>/* POSIX adds things to . */<br># include<br>#endif<br>...
It depends on the gcc-specific builtin limits.h to define some macros to work correctly,<br>on top of the use of the #include_next extension. Even clang has to work around this silliness.
SDL
SDL_endian.h has a goofy bit of feature detection for its byteswapping<br>functions. The purpose is to use compiler builtins or inline assembly whenever<br>possible, and only fall back to portable generic bitwise operations as a last resort. But the way<br>it goes about this is with the following logic:
if (GCC or clang) and __has_builtin(__builtin_bswapX) → use builtins
else if (msvc >= v8.0) -> use msvc intrinsic #pragma
else if defined (ISA-specific macro like __x86_64__) -> use inline assembly
else -> use generic impl with regular bitwise operations
This means that if you aren't GCC or clang, but you define the ISA-specific<br>predefined macro (for good reasons), it will try to use (extended) inline<br>assembly, even if you have the bswap builtins and provide the __has_builtin<br>special operator. Seems a little odd to expect an unknown compiler to...