全部十八个 DLL 在 Linux 上全部变绿
跨平台构建不只是拓宽你的覆盖范围——它逼着你的代码库不再依赖某一个编译器悄悄给的方便。你增加的每一个平台,都会让你已经拥有的那些平台变得更诚实。
Linux 构建有好几周一直处于「部分通过」的已知状态。十八个原生运行时 DLL(引擎的结构是一支由聚焦型共享库组成的舰队)中的大多数都能干净地构建。少数几个不行。失败的形态每次都一样:在明明存在于代码库里的函数上,链接期出现未定义符号错误。任何在 C++ 里做过 Linux 共享库的人,都知道这会通向哪里。
这个周六我坐下来,打算把这件事彻底了结。到周六结束时,全部十八个 DLL 都能干净地构建,Linux CI 流水线报告全绿。
这篇文章要讲的是,真正的问题出在哪里,修复方案长什么样,以及为什么我认为在三个平台(Linux、macOS、Windows MSVC)上构建是一份值得付出的纪律。
失败长什么样
模式每次都大致是这样。一个链接到比如说 libraku_runtime.so 的测试二进制文件会链接失败,报出类似这样的错误:
undefined reference to `raku_runtime_create_session(...)'
undefined reference to `raku_runtime_initialize(...)'
undefined reference to `raku_runtime_shutdown(...)'
这些函数确实存在,它们被写出来了,它们在源文件里,它们被编译过了,目标文件里包含了它们。但用 nm -D libraku_runtime.so 检查这个共享库时,它并没有导出这些函数。
这是经典的 Linux 共享库符号可见性问题。这个运行时在构建其源文件时使用了 CMake 标志 -fvisibility=hidden,这本身是一个相当不错的默认设置。-fvisibility=hidden 的意图,是把内部符号排除在共享库的公开符号表之外。任何需要公开的东西,都必须显式地打上 __attribute__((visibility("default"))) 标签,或者用一个会展开成它的宏。
这个运行时的构建系统有一个叫 RAKU_API 的宏,本应在每个平台上展开成正确的可见性属性。在 Windows 上,RAKU_API 在构建 DLL 时展开成 __declspec(dllexport),在使用 DLL 时展开成 __declspec(dllimport)。在 Linux 上,它本应在构建时展开成 __attribute__((visibility("default"))),在使用时展开成空。
问题在于,这个宏在 Linux 一侧并没有应用到每一个公开函数上。有些函数被打上了标签,很多没有。那些没被打标签的函数,就被 -fvisibility=hidden 这个默认设置隐藏了起来,从公开符号表里消失了。
修复方案长什么样
三个步骤,都是机械性的,都值得记下来。
第一:审计每一个公开 API 头文件,找出缺失 RAKU_API 标签的地方。 写了一个脚本,解析公开 API 中的每一个 .h 文件,找出每一个本应公开、却没有被打标签的函数声明。脚本会报告函数名、文件和行号。跑了这个脚本,得到一份清单:十八个 DLL 里共有三百四十七个函数需要打上标签。
第二:批量添加标签。 这正是智能体擅长处理的那种机械性重构。任务描述是:「对下面列出的每一个函数,在其声明那一行加上 RAKU_API。不要修改函数体,不要修改任何其他行。每完成一个子系统的一批之后跑一次构建,确认它在 Linux 上仍然能构建通过。」智能体干净利落地完成了这项工作,一个子系统接一个子系统。每个子系统的 PR 都可以独立评审,每合并一个,Linux 构建就更绿一分。
第三:一道防止回归的 CI 检查。 添加了一个在 PR 时运行的检查。这个检查在 Linux 上构建运行时,对每一个产出的 .so 运行 nm -D,把导出的符号列表和公开 API 头文件里声明的预期集合做比对,如果有任何预期符号缺失,就让这个 PR 失败。这正是那种能在「我忘了给一个新函数打标签」这类问题落地当天就抓住它,而不是几个月后才发现的护栏。
到周六结束时,全部十八个 DLL 都导出了完整的公开表面。测试二进制文件链接成功,测试跑通了,Linux 构建变绿了。
为什么三个平台值得付出这个代价
一个很自然的问题:为什么要费心搞 Linux?这个运行时的目标是 AR 眼镜,跑的是一个专用操作系统,既不是开发者机器意义上的 Linux,也不是 Windows 或 macOS。为什么要在支持真正目标硬件的成本之上,再额外承担跨平台的成本?
三个原因。
第一:服务端 AI 推理和云端渲染发生在 Linux 上。 任何一个 AR 体验的云端组件(模型服务、世界状态同步、持久化)都跑在 Linux 上。运行时有云端会调用的钩子。这些钩子必须能在 Linux 上构建和运行,云端才能完成集成。如果这个运行时是一个只支持 Windows 的庞然大物,云端要么得单独搭一个通信垫片,要么得在一个 Linux 兼容层下运行这个运行时。这两个都不是我希望合作伙伴去做的事。
第二:Linux 上的 CI 比 Windows 上的 CI 更快也更便宜。 我合并的每一个 PR 都会跑一遍 CI。Linux CI 运行器比 Windows CI 运行器更小、更快、更便宜。CI 循环越快,我和智能体们在一个周六里能跑的迭代次数就越多。把 Linux 作为一等构建目标,加速了整个开发工作流。
第三:跨平台纪律能抓住 bug。 这是最深层的原因。当一个代码库只在一个平台上构建时,开发者习惯用的模式就是那个平台上凑效的模式。跨平台构建逼着这些模式变得可移植:用显式的可见性属性取代隐式的,用显式的跨平台类型宽度取代「在这个编译器上 long 是 32 位」这种假设,用显式的线程语义取代「这在 Windows 上能用」这种想法。这次符号可见性的修复正是这种模式的体现。修复之后,代码库在每一个平台上都比修复之前更健壮了,因为这次修复让可见性契约变得显式了。
这能推广出什么普遍结论
几个诚实的模式。
Linux 上的符号可见性问题,是每个人都得交一次的税。 一个项目第一次撞上这个问题时,会觉得神秘又令人沮丧。一旦修复方案(RAKU_API 宏,一致地应用,配上一道 CI 护栏)到位,它就变得无形了。代价发生在第一次,早点交这笔税。
针对符号可见性回归的 CI 护栏不是可选项。 那种要花几个月才会浮出水面的 bug——因为没人在用某个函数,它缺个标签也没人注意——正是一道 CI 护栏能在它落地当天就抓住的那种 bug。把护栏设起来。
跨平台构建让代码库变得更诚实。 任何一个代码库只要依赖了某个平台的隐式行为,跨平台移植就会逼着这个行为变得显式。每一次我把一个代码库移植到一个新平台,新平台都会浮出一些原来的平台在悄悄吸收的 bug。这些修复是对每一个平台的改进,不只是对新平台的改进。
合作伙伴和构建者应该从中得到什么
如果你是一位正在为跨平台 AR 产品挑选引擎的合作伙伴,去问问那个团队他们的构建矩阵长什么样。一个在 Linux、macOS 和 Windows 上都构建的团队,是一个代码库被这三个平台之间的差异磨炼过纪律的团队。一个只在一个平台上构建的团队则不然。
如果你是一位在做自己的跨平台共享库项目的开发者,符号可见性审计是你今天就该跑的审计,而不是等到三月份某个测试因为要花三天才能诊断出的原因开始失败时再跑。对你的共享库跑一遍 nm -D,和你的头文件做比对,差异之处就是这次审计的结果。
如果你是一家 AI 实验室,你的编码智能体会写跨平台原生代码,那么这个可见性属性宏就是智能体的工作知识里应该具备的东西。一个写了新公开函数却忘了打标签的智能体,会给你带来一个后续 PR 的成本;一个一致地给每一个公开函数打标签的智能体,则物有所值。
周六收尾。十八个 DLL 在 Linux 上全部变绿,CI 现在更快了,代码库更诚实了,构建矩阵横跨三个平台。
回去继续构建。
一个被它触及的每一个平台磨炼出纪律的运行时
RakuAI 是面向 AR 眼镜、云端推理及其间一切的跨平台空间运行时——Linux、macOS 和 Windows,全部变绿。看看合作伙伴为什么要在一个可移植的基础之上构建。