返回 2026-04-15
⚙️ 工程

simdutf 现已无需依赖 libc++ 或 libc++abiSimdutf Can Now Be Used Without libc++ or libc++abi

mitchellh.com·2026-04-15

simdutf 库宣布支持在无 libc++ 或 libc++abi 的环境下运行,扩展了其跨平台兼容性。这一改进使得该高性能 UTF 转换库能够在更多嵌入式系统和轻量级环境中部署,而无需强制依赖特定 C++ 运行时库。

截至本 PR,simdutf 可以在不使用 libc++ 或 libc++abi1 的情况下使用。

simdutf 是 libghostty-vt2 中最后一个依赖的 libc++。在将 Ghostty 更新为使用此新的 simdutf 构建后,我们成功从依赖项中完全移除了 libc++ 和 libc++abi。

需要注意的是,本文撰写时,上游的 simdutf PR 尚未合并,我尚不清楚它是否会被采纳。初步反馈是积极的,但维护者可能出于任何原因选择不合并该 PR。Ghostty 项目已将其无 libc++ 的 simdutf 构建集成到主分支中。

不依赖 libc++ 的好处

不依赖 libc++ 使库更具可移植性(适用于嵌入式、WebAssembly、freestanding 环境),简化了交叉编译(无需目标特定的 C++ 标准库),减小二进制体积,并有助于简化静态链接。

作为通用且底层的库,simdutf 应尽可能具备可移植性和灵活性。如果下游用户能轻松使用 libc++,那当然很好!但如果不能,他们也不应因 simdutf 根本不需要它而被阻止使用。

libc++ vs libc++abi

要从程序中移除 libc++,有两个部分需要处理。

首先是 libc++,即 C++ 标准库,提供诸如 std::vector、std::string 等组件。如果你引入 <vector> 或即使是 libc 提供的 C++ 回退头文件如 <cstring>,你就依赖于 libc++。

更隐蔽的部分是 libc++abi,它提供了 C++ ABI,包括异常处理、虚函数表、RTTI 等功能。即使你完全没有引入任何 C++ 标准库头文件,只要使用了需要这些特性的 C++ 功能,就仍依赖于 libc++abi。

例如,下面这个极简的 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++ 特性的 C++ 库。尽管我不认识 Daniel Lemire 本人,但从他充分运用 C++ 特性的热情来看,simdutf 无疑是一个非常典型的 C++ 项目。

为了让我的改动有被接受的可能,我必须确保项目在不用 libc++ 的前提下仍能继续使用 C++ 特性,而不会带来麻烦。

STL 使用方式

我决定采取的方法是引入一个 stl_compat.h 头文件,集中管理所有 C++ 标准库类型。在正常的 libc++ 模式下,stl_compat.h 中的内容只是简单的包含或直接映射到对应的 C++ 标准库类型,运行时开销为零。

在 NO_LIBCXX 模式下,stl_compat.h 则提供 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 接口暴露了 C++ 类型如 std::string,在这些情况下必须打破 ABI。其他情况下则完全保留。

鉴于 SIMDUTF_NO_LIBCXX 是一个新增功能,会对编译单元产生影响,我认为仅在启用此标志时才打破 ABI 是可以接受的。在未启用 NO_LIBCXX 的现有情况下,ABI 完全保留,simdutf 可以更新而不破坏现有用户的 ABI。

我惊喜地发现,ABI 的破坏非常小,仅影响了一小部分诊断函数(例如获取活动实现名称)以及与其他 C++ 类型(如文本编码 std::string)交互的工具。根据定义,使用 SIMDUTF_NO_LIBCXX 的用户并不关心 libc++,因此这些 ABI 破坏反而更像是特性而非缺陷。

从 libc++abi 中剥离 simdutf

这项任务要复杂得多。

主要问题是,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 将每个后端建模为抽象实现接口的子类。这种设计可以保留,但抽象类的虚表仍会引用 __cxa_pure_virtual 来处理无法调用的纯虚函数入口。在 SIMDUTF_NO_LIBCXX 模式下,我选择提供一个微小的本地垫片,并确保运行时永远不会真正到达该位置。我将此符号标记为弱符号,以便当 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 性能如预期般未受影响。

拉取请求

让某件事正常工作是一回事,而让这件事被合并则是另一回事。

作为一名维护者,我非常清楚“这个能工作”和“这个可以合并”之间的巨大差距。我深知验证他人工作的难度,也明白如何确保未来能持续维护它。我也理解为何有人会提出大型 PR,却不清楚其目的何在。同时,我也承受着近期 AI 生成内容泛滥带来的负担。

因此,我付出了全明星贡献者理应得到的那种努力,并努力成为 simdutf 维护者的得力助手。

首先,我完整审查了整个 diff(是的,就是那约 3,000 行代码)。接着再次审查,并亲手重读了整个 diff 三到四次。我还根据一些本可能自己提出的意见进行了多次修改,即便从功能上看这些改动并无必要。

接下来,我手动撰写了一份详尽的 PR 描述,阐述了项目的动机、实现方式、局限性以及验证过程。我希望让维护者了解细节的同时,也能感受到我对这些细节所倾注的思考。

最后,我坦诚使用了 AI 辅助编写代码。但我明确表示:我已亲自审阅了所有内容,PR 描述和评论均未使用 AI 生成,且作为人类,我有能力也有信心为任何提议的修改进行辩护和调整。

颇具讽刺意味的是,完整的 diff 仅耗时约两小时完成,但额外的验证工作和 PR 准备工作却花费了我近三小时。相较于代码本身,我在维护人与人之间的边界上花费了更多时间,这理应出于对维护者投入心血的尊重。

最终状态

simdutf 的 PR 仍在审核中。初步反馈积极,我随时准备接受任何修改建议。当然,维护者也可能最终决定不予合并,这同样是可以接受的。

若您希望在无 libc++ 或 libc++abi 的环境下使用 simdutf,可暂时采用我的 fork。构建单文件加头文件的步骤与此前的说明一致。在编译 C++ 代码并包含头文件时,只需定义 SIMDUTF_NO_LIBCXX 宏即可获取无 libc++ 版本的库。

Ghostty 的 PR 现已合并。因此,libghostty-vt 在 SIMD 构建中不再依赖 libc++ 或 libc++abi。

脚注

  • libc++ 是 C++ 标准库(例如 std::vector、std::string 等),而 libc++abi 则是 C++ ABI 库(例如异常处理、RTTI 等)。↩
  • 请注意,即使禁用 SIMD,libghostty-vt 始终没有任何依赖项,甚至不依赖 libc。它是一个完全独立的库。↩
  • 需要完整排版与评论请前往来源站点阅读。