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

Lobsters Hottest 新闻

摘要

本文讨论了编写可移植C代码的实际挑战,这些挑战源于对非标准编译器扩展和glibc条件头文件的依赖,并通过构建C编译器的示例进行说明。

<p><a href="https://lobste.rs/s/mcgtsh/on_c_extensions_portability_alternative">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/25 15:09

# 关于C扩展、可移植性与替代编译器 来源:https://lemon.rip/w/6-c-extensions-compilers/ 2026年5月24日—c (https://lemon.rip/tags/c) compilers (https://lemon.rip/tags/compilers) 写过C的人都知道,完全符合ISO C标准的代码是不切实际的稀有物。现实世界中的大部分C代码都在不同程度上依赖非标准行为和语言扩展,其中许多并非为了额外功能,而只是为了绕开不同编译器和库中的缺陷与漏洞。许多代码库会尝试通过预处理器检查和防护来部分支持多种环境,但这些尝试要么很脆弱,要么直接出错。我在开发自己的C编译器(https://codeberg.org/lsof/antcc)时遇到了许多这样的情况,以下是一些例子汇总。 ## glibc 系统的C库头文件是一个希望变得有用的C编译器遇到的第一个“障碍”。如果不能预处理和解析 `#include <stdio.h>`,那就连hello world都过不了。因为我用的是GNU/Linux,所以这意味着glibc。公平地说,glibc*确实*尝试在非GCC编译器上保持其头文件的兼容性。在 `sys/cdefs.h`(https://sourceware.org/git/?p=glibc.git;a=blob;f=misc/sys/cdefs.h;hb=66f3e9219d)这个庞然大物(它会被每个libc头文件间接包含)中,他们使用了各种针对编译器预定义宏的预处理器检查,以确定支持哪些编译器扩展,当不支持时就用 `#define` 去掉它们。不幸的是,这有时候就是会出错。例如,在Linux上,`sys/epoll.h` 中的 `struct epoll_event` 是一个** packed struct**(https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/sys/epoll.h;hb=66f3e9219d8f86b#l85),它使用了GNU的 `__attribute__((packed))`。因为这会改变结构体布局(在64位系统上),你不能忽略它而不破坏ABI。所以好吧,假设你在自己的编译器中实现了对 `__attribute__((packed))` 的支持。但这还不够,因为前面提到的 `sys/cdefs.h` 包含了这样的代码: ```c /* GCC, clang, and compatible compilers have various useful declarations that can be made with the '__attribute__' syntax. All of the ways we use this do fine if they are omitted for compilers that don't understand it. */ #if !(defined __GNUC__ || defined __clang__ || defined __TINYC__) # define __attribute__(xyz) /* Ignore */ #endif ``` 如果你不是gcc、clang或tcc(https://www.bellard.org/tcc/),那就自求多福了。当然,`epoll`头文件是Linux特有的,所以你可以辩解说用C标准可移植性标准来要求它并不公平。 有些C头文件应该由编译器提供,因为它们即使在**独立实现**(https://en.cppreference.com/c/language/conformance)上也应当存在,并且依赖于编译器的内部定义。例如在我的电脑上,这些头文件位于GCC的 `/usr/lib/gcc/x86_64-pc-linux-gnu/16.1.1/include/` 和clang的 `/usr/lib/clang/22/include/` 下。这些内置头文件包括 `stddef.h`、`stdint.h`、`limits.h`、`float.h` 等。然而,POSIX要求 `limits.h` 除了定义标准C常量外,还要定义一些POSIX特有的常量。所以,你仍然需要在一个平台特定的 `limits.h` 之上再加一个编译器的。glibc 的 `<limits.h>` 看起来像这样(精简版): ```c ... /* If we are not using GNU CC we have to define all the symbols ourself. Otherwise use gcc's definitions (see below). */ #if !defined __GNUC__ || __GNUC__ < 2 /* We only protect from multiple inclusion here, because all the other #include's protect themselves, and in GCC 2 we may #include_next through multiple copies of this file before we get to GCC's. */ # ifndef _LIMITS_H # define _LIMITS_H 1 /* We don't have #include_next. Define ANSI for standard 32-bit words. */ /* These assume 8-bit `char's, 16-bit `short int's, and 32-bit `int's and `long int's. */ # define CHAR_BIT 8 ... # endif /* limits.h */ #endif /* GCC 2. */ #endif /* !_LIBC_LIMITS_H_ */ /* Get the compiler's limits.h, which defines almost all the ISO constants. We put this #include_next outside the double inclusion check because it should be possible to include this file more than once and still get the definitions from gcc's header. */ #if defined __GNUC__ && !defined _GCC_LIMITS_H_ /* `_GCC_LIMITS_H_' is what GCC's file defines. */ # include_next #endif /* The files in some gcc versions don't define LLONG_MIN, LLONG_MAX, and ULLONG_MAX. */ #if defined __USE_ISOC99 && defined __GNUC__ # ifndef LLONG_MIN # define LLONG_MIN (-LLONG_MAX-1) # endif ... #endif #ifdef __USE_POSIX /* POSIX adds things to <limits.h>. */ # include <bits/posix1_lim.h> #endif ... ``` 它依赖GCC特有的内置 `limits.h` 来正确定义某些宏,除此之外还使用了 `#include_next` 扩展。就连**clang**也不得不设法绕过这种愚蠢的行为(https://github.com/llvm/llvm-project/blob/85c3fd048d7df66d093bfaf45e7c3c3ec44122bf/clang/lib/Headers/limits.h#L16)。 ## SDL SDL_endian.h 中的字节交换函数(https://github.com/libsdl-org/SDL/blob/1df9ae4338c43ad9dce4b27a77f807aa8d2b073b/include/SDL_endian.h#L132-L207)有一个略显滑稽的功能检测逻辑。其目的是尽可能使用编译器内建函数或内联汇编,只有在万不得已时才回退到可移植的通用位操作。但它的做法是这样的: 1. 如果(GCC或clang)并且 `__has_builtin(__builtin_bswapX)` → 使用内建函数 2. 否则如果(msvc >= v8.0)→ 使用msvc intrinsic #pragma 3. 否则如果定义了(ISA特定宏如 `__x86_64__`)→ 使用内联汇编 4. 否则 → 使用常规位操作的通用实现 这意味着如果你不是GCC或clang,但出于(合理的原因)定义了ISA特定的预定义宏,它会尝试使用(扩展)内联汇编,即使你提供了bswap内建函数并实现了 `__has_builtin` 特殊操作符。期望一个未知的编译器支持GCC风格的扩展内联汇编,这似乎有点奇怪。 ## OpenBSD libc 一些OpenBSD头文件(https://codeberg.org/OpenBSD/src/src/commit/222ca78cfc16a75219b890afea5c44781ee18f9c/include/signal.h#L74-L113)包含了内联函数定义,这些定义旨在让编译器在优化时选择性地使用。它们用宏 `__only_inline` 定义,例如: ```c __only_inline int sigemptyset(sigset_t *__set) { *__set = 0; return (0); } ``` 并且当/如果编译器没有真正内联它们时,应该回退到“真正的”外部符号。换句话说,就是带有外部链接的内联函数。这些通常是一团乱麻:虽然它们在C99中有规定,但标准行为与C99之前非标准的GCC行为(4.2之前的默认行为)冲突。简单来说,头文件中的内联定义应该使用 `extern inline` 并带上函数体,这样就不会产生实际的导出函数;而在翻译单元中,只使用 `inline` 来声明该函数,从而在那里导出其定义。更让人困惑的是,`inline` 在C++和C中的含义不同。请参阅Youtao Guo的这篇好文章(https://coyorkdow.github.io/c/2021/11/17/C_inline.html)。 因此,OpenBSD依赖于GCC的内联语义,为了掩盖GCC版本间的差异,`sys/cdefs.h`(https://codeberg.org/OpenBSD/src/src/commit/222ca78cfc16a75219b890afea5c44781ee18f9c/sys/sys/cdefs.h#L158-L173)中的 `__only_inline` 宏在较新的GCC版本上显式使用 `__attribute__` 来指定旧的gnu89内联语义。但在非GNU编译器上,它被定义为 `static` 链接,这会导致问题,因为这样会在声明/定义函数时出现冲突的链接类型。幸运的是,它们尊重一个宏 `_ANSI_LIBRARY`,当定义了该宏时,会完全省略在标准头文件(如 `signal.h`)中使用这些糟糕的 `__only_inline` 定义。所以你拿不到“优化版本”(可能差别不大),但至少能工作。 我还在构建Guile和nano时遇到了**Gnulib**(https://www.gnu.org/software/gnulib/)为 `extern inline` 提供的兼容性代码,这突显了C这个角落案例各种有缺陷和奇怪的实现。参见 extern-inline.m4(https://codeberg.org/guile/guile/src/commit/1819c8152d8c0436a590b02653eb71c360074b1b/m4/extern-inline.m4#L31-L137)的解释注释,这里摘录一段: ```c #if (((defined __APPLE__ && defined __MACH__) \ || defined __DragonFly__ || defined __FreeBSD__) \ && (defined HAVE___HEADER_INLINE \ ? (defined __cplusplus && defined __GNUC_STDC_INLINE__ \ && ! defined __clang__) \ : ((! defined _DONT_USE_CTYPE_INLINE_ \ && (defined __GNUC__ || defined __cplusplus)) \ || (defined _FORTIFY_SOURCE && 0 < _FORTIFY_SOURCE \ && defined __GNUC__ && ! defined __cplusplus)))) # define _GL_EXTERN_INLINE_STDHEADER_BUG #endif #if ((__GNUC__ \ ? (defined __GNUC_STDC_INLINE__ && __GNUC_STDC_INLINE__ \ && !defined __PCC__) \ : (199901L <= __STDC_VERSION__ \ && !defined __HP_cc \ && !defined __PGI \ && !(defined __SUNPRO_C && __STDC__))) \ && !defined _GL_EXTERN_INLINE_STDHEADER_BUG) # define _GL_INLINE inline # define _GL_EXTERN_INLINE extern inline # define _GL_EXTERN_INLINE_IN_USE #elif (2 < __GNUC__ + (7 <= __GNUC_MINOR__) && !defined __STRICT_ANSI__ \ && !defined __PCC__ \ && !defined _GL_EXTERN_INLINE_STDHEADER_BUG) # if defined __GNUC_GNU_INLINE__ && __GNUC_GNU_INLINE__ /* __gnu_inline__ suppresses a GCC 4.2 diagnostic. */ # define _GL_INLINE extern inline __attribute__ ((__gnu_inline__)) # else # define _GL_INLINE extern inline # endif # define _GL_EXTERN_INLINE extern # define _GL_EXTERN_INLINE_IN_USE #else # define _GL_INLINE _GL_UNUSED static # define _GL_EXTERN_INLINE _GL_UNUSED static #endif ``` ……好吧。真是可爱。 ## bionic **bionic**(https://android.googlesource.com/platform/bionic/)是Android的libc。别出心裁地,它的头文件大量假设clang而不是gcc。里面充满了clang特有的扩展,比如 `_Nonnull`、`_Null_unspecified`¹(https://lemon.rip/w/6-c-extensions-compilers/#fn-1),用于**可空性检查**(https://clang.llvm.org/docs/analyzer/developer-docs/nullability.html),以及其他东西。幸运的是,通过命令行标志把这些宏 `#define` 掉并不难。另外,我遇到这个的唯一原因是我在用我的Android手机配合 **Termux**(https://termux.dev/en/) 作为原生aarch64开发环境(笑),而它使用的是bionic头文件。 ## 结论:我意识到我主要写了关于libc头文件的内容,但实际上我还可以举出无数例子,不过我很快就要考试了,我已经拖延得够久了。虽然许多开源项目为了非关键功能依赖编译器特定的非标准扩展和行为,这很烦人,但要求每个开发者用不同的编译器(包括小众或体积小的)来测试他们的C代码也不公平。C的可移植性本身就已经很困难了。 从一个编写编译器的人的角度来看,可能的解决方案有: 1. 尝试向上游修复这些不兼容问题。 2. 积累足够的知名度,使开发者愿意主动添加专属的 `#ifdef` 检查并默认用你的编译器进行测试。 3. “下游”处理这些问题,或许分发一些**补丁**(https://github.com/search?q=repo%3Aoasislinux/oasis%20path%3A.patch&type=code)(https://git.sr.ht/~jprotopopov/kefir/tree/15dfd84a8dd6832b3bffcd6284b55afce5ba931e/item/source/tests/external/sdl2/sdl2-2.30.10.patch#L8)。 4. 假装自己是(某个版本的)GCC并实现其扩展。 (1)似乎是一场必败之战,(3)是最容易的,(4)是现实可行(尽管费力)的方法,可以支持大量代码库,而对你编译器的用户以及那些代码库的开发者影响最小。例如,clang定义了 `__GNUC__=4`(以及 `__GNUC_MINOR__=2`,`__GNUC_PATCHLEVEL__=1`)来声称与GCC 4.2.1兼容。尽管此时clang已经达到了(2)的状态,但为了能让clang编译Linux内核(两个项目都需要打补丁)还是付出了巨大努力。 当然,(4)的一个问题是,许多代码库会检查 `#ifdef __GNUC__`,如果定义了该宏,它们会自由使用各种更新的GCC扩展,而不做版本检查。于是你不得不一直追赶上,这就是为什么clang即使支持比GCC 4.2.1更新的GNU扩展,也不提高 `__GNUC__` 宏的原因(参见此讨论(https://discourse.llvm.org/t/rfc-bump-up-clangs-gnuc-minor/23084/6))。 理想情况下,像 `__has_builtin`、`__has_feature`、`__has_attribute` 这样的功能测试宏,甚至标准的 `__STDC_NO_VLA__`,应该被更广泛地使用,而不是用编译器特有的防护和版本检查。就目前而言,GCC/clang的准双头垄断在\*NIX环境下是现状,无论好坏。 向那些维护独立小型C编译器的开发者致敬:**tcc**(https://www.bellard.org/tcc/)、**cproc**(https://git.sr.ht/~mcf/cproc)、**scc**(https://www.simple-cc.org/)、**vbcc**(http://www.compilers.de/vbcc.html)、**nwcc**(https://nwcc.sourceforge.net/)、**kefir**(https://kefir.protopopov.lv/),以及更多(https://github.com/ethanc8/awesome-small-compilers/blob/master/SmallCCompilers.md)。 1. 即 `__BIONIC_COMPLICATED_NULLNESS`。令人惊叹(https://android.googlesource.com/platform/bionic/+/refs/tags/android-15.0.0_r23/libc/include/sys/cdefs.h#65)。↩(https://lemon.rip/w/6-c-extensions-compilers/#fr-1-1)

相似文章

使用并行Claude团队构建C编译器

Anthropic Engineering

Anthropic研究员展示了如何使用16个并行Claude实例自主构建一个基于Rust的C编译器,该编译器能够编译Linux内核。文章详细介绍了这一多智能体自主编码实验的架构、成本和经验教训。

Common Lisp 可移植性库状态

Lobsters Hottest

Common Lisp 可移植性库的全面状态概览,展示了不同 Common Lisp 实现之间的兼容性百分比。

用 Zig 写一个 C 编译器

Hacker News Top

一位开发者记录了用 Zig 语言、按照 Nora Sandler 的教程系列构建名为 paella 的 C 编译器的全过程。

C++ 编译器何时可以反虚拟化调用?

Hacker News Top

探讨 C++ 编译器何时可以对虚函数调用进行去虚拟化,涵盖已知动态类型和 final 关键字等情况,并在 GCC、Clang、MSVC 和 ICC 之间进行比较。