C 扩展、可移植性和替代编译器

2026-05-25 1 阅读 xngbuilds
关于 C 扩展、可移植性和替代编译器 2026 年 5 月 24 日 — c 编译器 任何编写过 C 语言的人都知道,完整的符合 ISO C 标准的代码非常罕见。大多数现实世界的 C 代码在不同程度上依赖于非标准行为和语言扩展,其中很多并不是为了额外的功能,而只是为了解决不同编译器和库中的错误和差距。许多代码库会尝试在某种程度上支持各种环境,主要是通过使用预处理器检查和防护,但这些尝试充其量是挑剔的,最坏的情况是直接被破坏。我在使用 C 编译器时遇到过许多这样的情况,所以这里列出了其中的一些情况。 glibc 系统的 C 库头文件是 C 编译器想要发挥作用的第一个“障碍”。如果您无法预处理和解析 ,您将无法通过 hello world。因为我使用 GNU/Linux,所以这意味着 glibc。现在,值得赞扬的是,glibc 确实尝试在非 GCC 编译器上保留其标头的兼容性。在 sys/cdefs.h 这个庞然大物中,每个 libc 头文件都间接包含它,它们对编译器预定义的宏使用各种预处理器检查来确定支持哪些类型的编译器扩展,并在不支持时将其#define 掉。不幸的是,这有时会被打破。例如,在 Linux 上 sys/epoll.h 中的 struct epoll_event 是一个打包的 struct ,它使用 GNU __attribute__((packed)) 。因为这会更改结构布局(在 64 位上),所以您不能在不破坏 ABI 的情况下忽略它。好吧,假设您在编译器中实现了对 __attribute__((packed)) 的支持。但这还不够,因为前面提到的 sys/cdefs.h 包含以下代码: /* GCC、clang 和兼容编译器具有可以使用 '__attribute__' 语法进行的各种有用的声明。如果编译器不理解的话,我们使用的所有方法都可以正常工作。 */ #if !(define __GNUC__ || Defined __clang__ || Defined __TINYC__) # Define __attribute__(xyz) /* Ignore */ #endif 如果你不是 gcc、clang 或 tcc,那你就运气不好了。尽管 epoll 标头是 Linux 特定的,所以您可能会认为应用 C 标准可移植性标准是不公平的。一些 C 头文件应该由编译器提供,因为即使在独立实现上它们也应该存在,并且依赖于编译器内部定义。例如,在我的计算机中,对于 GCC,它们位于 /usr/lib/gcc/x86_64-pc-linux-gnu/16.1.1/include/ ,对于 clang,它们位于 /usr/lib/clang/22/include/ 。这些内置标头包括 stddef.h 、 stdint.h 、 limit.h 、 float.h 等。但是,除了标准 C 常量之外,POSIX 还需要在 limit.h 中定义一些特定于 POSIX 的常量。因此,您仍然需要在编译器之上添加特定于平台的 limit.h 。 glibc 的 看起来像这样(缩写): ... /* 如果我们不使用 GNU CC,我们必须自己定义所有符号。否则使用 gcc 的定义(见下文)。 */ #if !define __GNUC__ || __GNUC__ < 2 /* 我们在这里只防止多重包含,因为所有其他 #include 都会保护自己,并且在 GCC 2 中,我们可以在进入 GCC 之前通过该文件的多个副本 #include_next 。 */ # ifndef _LIMITS_H #define _LIMITS_H 1 /* 我们没有#include_next。为标准 32 位字定义 ANSI 。 */ /* 这些假设为 8 位 `char'、16 位 `short int'、32 位 `int' 和 `long int'。 */ #define CHAR_BIT 8 ... #endif /* Limits.h */ #endif /* GCC 2. */ #endif /* !_LIBC_LIMITS_H_ */ /* 获取编译器的limits.h,它定义了几乎所有的ISO常量。我们将此 #include_next 放在双重包含检查之外,因为应该可以多次包含此文件,并且仍然可以从 gcc 的标头中获取定义。 */ #if Defined __GNUC__ && !define _GCC_LIMITS_H_ /* `_GCC_LIMITS_H_' 是 GCC 文件定义的。 */ # include_next #endif /* 某些 gcc 版本中的 文件未定义 LLONG_MIN、LLONG_MAX 和 ULLONG_MAX。 */ #if Defined __USE_ISOC99 && Defined __GNUC__ # ifndef LLONG_MIN # Define LLONG_MIN (-LLONG_MAX-1) # endif ... #endif #ifdef __USE_POSIX /* POSIX 将内容添加到 中。 */ # include #endif ... 除了使用 #include_next 扩展之外,它还取决于 gcc 特定的内置 limit.h 来定义一些宏以便正确工作。甚至 clang 也必须解决这种愚蠢的问题。 SDL SDL_endian.h 对其字节交换函数进行了一些愚蠢的功能检测。目的是尽可能使用编译器内置函数或内联汇编,并且仅在万不得已时才使用可移植通用位操作。但它的实现方式是使用以下逻辑: if (GCC 或 clang) 和 __has_builtin(__builtin_bswapX) → 使用内置函数 else if (msvc >= v8.0) -> 使用 msvc 内部 #pragma else if 定义(ISA 特定的宏,如 __x86_64__ ) -> 使用内联汇编 else -> 使用常规按位运算的通用 impl 这意味着