simdutf 现在无需 libc++ 或 libc++abi 即可使用
原文由 Mitchell Hashimoto 于 发布,订阅该博客
自此 PR 起,simdutf 已可在无需 libc++ 或 libc++abi 的情况下使用1。
simdutf 是 libghostty-vt 中最后一个依赖 libc++ 的组件2。在将 Ghostty 更新至使用这一新版 simdutf 构建后,我们得以从依赖中彻底移除了 libc++ 和 libc++abi。
不依赖 libc++ 的好处
不依赖 libc++ 可以让库更具可移植性(适用于嵌入式、WebAssembly、无托管环境等)、简化交叉编译(无需针对目标平台的 C++ 标准库)、减小二进制体积,并能简化静态链接。
作为一个通用型底层库,simdutf 应当尽可能可移植、灵活。如果下游使用者能够方便地使用 libc++,那当然很好;但如果不能,也不该因此无法使用 simdutf——毕竟从根本上说,它并不需要这些依赖。
libc++ vs libc++abi
要让程序摆脱对 libc++ 的依赖,需要处理两个部分。
首先,libc++ 是 C++ 标准库,提供了 std::vector、std::string 等。如果引入了 <vector>,甚至是像 <cstring> 这类对 C 库的 C++ 封装头文件,就已经依赖了 libc++。
更隐蔽的部分是 libc++abi,它提供 C++ ABI,包括异常处理、虚函数表、RTTI 等。只要使用了任何需要这些机制的 C++ 特性,就会依赖 libc++abi,哪怕完全没有引入任何 C++ 标准库头文件。
例如,下面这段极简的 C++ 程序就依赖于 libc++abi,因为它使用了函数内静态变量,而这类变量需要线程安全的初始化,这正是 C++ ABI 的一部分:
struct Implementation {
int version;
};
const Implementation& get_impl() {
static const Implementation impl{1};
return impl;
}
int main() {
return get_impl().version;
}让 simdutf 摆脱对 libc++ 的依赖
我们先谈 libc++(而非 ABI)。
simdutf 是一个大量使用最新、最前沿 C++ 特性的库,虽然我与 Daniel Lemire 并不相识,但他似乎非常喜欢将 C++ 特性发挥到极致。因此,simdutf 彻头彻尾是一个 C++ 项目。
要让我的改动有机会被接受,就必须确保项目仍能无障碍地继续使用 C++ 特性。
STL 用法
我采取的方案是引入一个stl_compat.h 头文件来集中管理所有 C++ 标准库类型。在常规的 libc++ 模式下,stl_compat.h 中的所有内容都只是对相应 C++ 标准库类型的简单引入或极简别名,没有任何运行时开销。
在 NO_LIBCXX 模式下,stl_compat.h 则为 simdutf 所用到的 C++ 类型提供了自有的实现,但仅实现到刚好满足 simdutf 需求的程度。例如,stl_compat.h 提供了对 std::pair 的自有实现。
因此,整个 diff 中的改动大多呈现为如下形式:
-std::pair<const char *, char32_t *>
+internal::pair<const char *, char32_t *>
arm_convert_latin1_to_utf32(const char *buf, size_t len,
char32_t *utf32_output) {ABI 兼容性
我的目标是尽可能保留 ABI 兼容性。部分公开 ABI暴露了 std::string 等 C++ 类型,在这些场景下 ABI 不得不被打破。除此之外,则完全得以保留。
鉴于 SIMDUTF_NO_LIBCXX 是一个会对编译单元产生影响的新特性,我认为仅在启用该标志时打破 ABI 是可以接受的。在未启用 NO_LIBCXX 标志的现有情况下,ABI 完全保留,现有用户更新 simdutf 时不会遭遇 ABI 破坏。
令人欣喜的是,ABI 的破坏非常轻微,仅涉及少数诊断函数(例如获取当前实现名称的函数)以及用于处理其他 C++ 类型(如文本编码 std::string)的辅助函数。由于使用 SIMDUTF_NO_LIBCXX 的人本来就不关心 libc++,这些 ABI 变化与其说是缺陷,不如说是特性。
让 simdutf 摆脱对 libc++abi 的依赖
这项任务要复杂得多。
主要问题在于,libc++abi 的依赖通常不会以明显的源码级引入形式出现。它们之所以会出现,是因为编译器会为一些看似普通的语言特性悄悄生成对 C++ ABI 运行时的调用。为了检测这些依赖,我不得不编写一个脚本来反编译目标文件,并查找诸如 __cxa_guard_acquire 之类的符号。
simdutf 中最大的“元凶”是运行时分发层。原始代码大量依赖函数内静态变量。在 C++ 中,这些局部变量由线程安全初始化辅助函数(如 __cxa_guard_acquire 和 __cxa_guard_release)来保护,而这些函数由 C++ ABI 运行时提供。因此,即使代码中完全没有提及 libc++abi,编译后的目标文件仍然依赖它。在 NO_LIBCXX 模式下的修复方法,是改用翻译单元级静态变量来替代函数内静态变量。
#if SIMDUTF_IMPLEMENTATION_ICELAKE
#ifdef SIMDUTF_NO_LIBCXX
static const icelake::implementation icelake_singleton{};
#endif
static const icelake::implementation *get_icelake_singleton() {
#ifdef SIMDUTF_NO_LIBCXX
return &icelake_singleton;
#else
static const icelake::implementation icelake_singleton{};
return &icelake_singleton;
#endif
}
#endif其次,simdutf 将每个后端都建模为抽象 implementation 接口的子类。这一设计可以保留,但抽象类的虚表仍会为那些不可能被调用的纯虚函数条目引用 __cxa_pure_virtual。在 SIMDUTF_NO_LIBCXX 模式下,我选择提供一个极小的本地垫片,并确保运行时永远不会真正执行到它。我将其标记为 weak 符号,以便在存在 C++ ABI 时能被其覆盖。
#ifdef SIMDUTF_NO_LIBCXX
// The abstract implementation vtable still carries pure-virtual slots even
// though correct dispatch never reaches them in this build mode. Provide the
// narrowest possible ABI shim so stricter no-libcxx objects do not require
// libc++abi just for this unreachable hook. Keep it weak so a toolchain's real
// libc++abi definition wins if one is linked in anyway.
extern "C" SIMDUTF_WEAK [[noreturn]] void __cxa_pure_virtual() noexcept {
__builtin_trap();
}
#endif最后,我编写了一个脚本,在使用 -fno-exceptions 和 -fno-rtti 编译时审计构建产物,检查是否出现 __cxa_guard_*、__gxx_personality、__cxa_throw、typeinfo 或 __dynamic_cast 等符号。该脚本已加入 simdutf 的 CI,以确保未来在 NO_LIBCXX 构建中不会意外重新引入对 libc++abi 的依赖。
验证
内部验证
simdutf 是一个对正确性和性能要求极高的库,因此我必须确保改动不会影响这两方面。我修改了既有的测试和基准测试套件,使其能在 NO_LIBCXX 和常规两种模式下运行,并确认所有测试均通过、基准测试结果不受影响。
关键在于,我已提交了确保 NO_LIBCXX 模式与现有测试和基准测试套件兼容所需的改动。这意味着未来对 simdutf 的任何改动都可以继续对两者进行验证。
外部验证:Ghostty
接着,我将 Ghostty 更新为使用我 fork 中的新版 simdutf,将我们的构建改为使用 SIMDUTF_NO_LIBCXX,并加入了一套自有测试来验证构建产物不再依赖 libc++ 或 libc++abi。
Ghostty 拥有一套完善的测试来验证我们的 UTF-8 解码行为(尤其是对非法输入的处理),还有一套内置的基准测试套件来测试各种场景下的 UTF-8 吞吐量。我运行了全部 Ghostty 测试和基准测试,验证了它们全部通过,且如预期那样,UTF-8 性能未受影响。
Pull Request
让代码跑起来和让代码被合并是两回事。
作为维护者,我太清楚“能跑”和“能合入”之间的差距。我深知验证他人工作、并对后续维护抱有信心的挑战;也明白提交一个大型 PR 却说不清动机时的困难;更了解近期 AI 生成的低质内容所带来的负担。
因此,我付出了我期望从一位顶尖贡献者身上看到的努力,并努力为 simdutf 的维护者成为这样的人。
首先,我完整审阅了整个 diff(没错,足足约 3,000 行)。然后又审阅了一遍。我前后手写重读了整个 diff 三四遍。即便功能上没有问题,我也基于那些我自己可能会提出评论的地方做了多处修改。
接着,我亲手撰写了详细的 PR 描述,阐释了动机、方案、局限性和验证方式。我希望确保维护者既能了解细节,也能感受到我在细节上投入的思考。
最后,我声明了我确实使用了 AI 来辅助编写代码。但我也明确表示,我已人工审阅了所有内容,PR 描述和评论均未使用 AI 撰写,并且作为人类,我有能力也有信心为所有提议的改动进行辩护和修改。
颇为讽刺的是,整理完整 diff 只花了我约 2 小时,而额外的验证工作和 PR 准备却花了约 3 小时。我在与人协作的边界上花费的时间比写代码本身还多——而出于对维护者投入心血的尊重,本就该如此。
最终状态
simdutf PR 仍在审核中。初步反馈是积极的,我也愿意接受任何所要求的修改。维护者也可能最终决定不合并,这也没关系。
如果你想在不依赖 libc++ 或 libc++abi 的情况下使用 simdutf,目前可以使用我的 fork。生成单文件合并版加头文件的构建方式不变。在编译 C++ 代码并引入头文件时,只需定义 SIMDUTF_NO_LIBCXX 即可获得无 libc++ 版本的库。
Ghostty PR 现已合并。因此,libghostty-vt 在 SIMD 构建中已不再依赖 libc++ 或 libc++abi。
脚注
随机一篇博客
评论
登录后参与讨论