Skip to content

Commit 6108d34

Browse files
C 库要按它下面实际有的东西被配置 —— 以及那道守卫用错了判据 (#4)
* fix: 那十五个间接符号是编译器的产物,不是这个 port 的性质 链接线上的 --ld-path 读起来像「这个格式需要那个系统的链接器」,而重测表明 它的适用范围窄得多。 重测(2026-08-22,Linux,开源工具链): 1336 个 musl 单元 + 11 个 port 单元 + 9 个实现单元 = 1356 个对象, 为 arm64 与 x86_64 两个目标各编一遍,再换 clang 22.1.8 编一遍, llvm-nm 数到的间接符号都是 0;而 lld 22.1.8 里那句 TODO 还在。 两者不矛盾:链接器的限制仍在,只是编译器不再产生够得着它的构造。那十五个 符号属于原测量所用的编译器 —— 那个系统自带的那个 —— 不属于这个 port, 不属于 musl,也不属于目标格式。 于是 ld64.lld 在 Linux 上就能把它们链成一个那个系统的可执行文件, --ld-path 只在那个系统上用它自己的编译器原生构建时才需要。 tools/probe-cross-macos.sh 复现这两次测量,并且已经验证它会变红: 往 port/src 里加一个规范外的名字,两条断言和链接同时失败。 * deps: 改用 git + 分支跟随这条开发线 四条依赖全部指向 feat/openkal-closure。已实测整张图能解析并构建: mcpp 逐个克隆分支,openkal-musl → openkal-linux → openkal 三层建成, examples/posix 的 32 项断言 0 failures。 * fix: 内部头是 C,而消费者可以不是 musl 的构建通过 src/include 拿到自己的声明,那些头写成 C,把 restrict 当关键字 裸用;公开头用的是 __restrict,两种语言都接受。 这个包发布的正是它构建所用的那一套目录 —— 一套而不是两套,那是 mcpp.toml 记下的 决定 —— 所以 C++ 消费者也会够到 src/include。那里 restrict 不是关键字,于是 hidden struct tm *__gmtime_r(const time_t *restrict, struct tm *restrict); 被读成两个都叫 restrict 的参数,编译器判为重定义。实测:一个 include <vector> 的 C++ 翻译单元在碰到自己的任何东西之前就报四个这样的错。 关键字在这里给一个拼法,而不是去改 musl —— 另一条路是编辑这个 port 原样编译的 那 1345 个源文件。 ⓘ 这是次优解。最优解是让一个包区分「它构建所用的目录」与「它发布的目录」, 构建工具今天表达不了;有了它,内部覆盖层根本到不了消费者,也就不需要任何拼法。 注在这里,免得更好的修法被丢掉。 * fix: 内部头是 C,而消费者可以不是 —— 给出规则而不是逐条创可贴 musl 的构建通过 src/include 拿到自己的声明。这个包发布的正是它构建所用的那套 目录,所以消费者也会够到 src/include —— 而那里的一切都是为 C 写的。 规则:musl 自己的源文件是 C,永远不会被当成别的编译。所以一个 C++ 翻译单元 必然不是它们中的一个,src/include 为 musl 自己加的东西在这里全都是惰性的, 它唯一不该做的事是和消费者相撞。 三个名字,而且是被两个消费者一个一个撞出来的,不是读出来的: restrict C 的关键字,C++ 的不是。f(int *restrict, int *restrict) 被读成 两个都叫 restrict 的参数。给它一个两种语言都接受的拼法。 hidden, weak musl 写成裸词的属性。置空而不是删掉,因为 src/include 的声明 在用它们,而一个 include <pthread.h> 的消费者会读到那些声明。 weak_alias 删掉而不是置空。它只出现在 musl 的 .c 里、任何头都不用它, 而留着会弄坏任何自己写 __attribute__((weak_alias(...))) 的 消费者 —— LLVM 的 libunwind 在十五处这么写,报的是 「use of undeclared identifier __weak__」。 ⓘ 这仍是次优解。最优解是让一个包区分「它构建所用的目录」与「它发布的目录」。 实测 2026-08-22:mcpp 表达不了 —— 把那两个目录挪进按 glob 限定的 flags, 它们在命令行上落到 include_dirs 之后,musl 自己的构建于是先找到公开的 <features.h>,报 `unknown type name hidden'。注在这里,免得更好的修法被丢掉。 验证:musl 构建通过;examples/posix 32 项断言 0 failures。 * fix: hidden 在 C++ 下给 C 链接,而不是置空 内部覆盖层加的每一条声明都以 hidden 开头 —— 那正是它的用途 —— 所以一个拼法 就让它们全部拿到它们本来被写成的链接。 覆盖层的头自己不带 extern "C",因为 musl 从不把它们当 C++ 编;一个把 hidden 置空的 C++ 消费者会把声明解析过去,然后在链接时对不上 musl 的定义。实测: undefined symbol: ___errno_location() did you mean: extern "C" ___errno_location * fix: 程序头的指针丢了,而缺席被当成了答案 musl 从辅助向量回答 dl_iterate_phdr。这个 port 替换 __libc_start_main 正是为了 不去读那个向量 —— openkal 的实现没有义务留下一个,而这个包构建的三种格式里有 两种根本没有这种东西。替换之后,这条查询就没东西可报了。 ⚠️ 而它把缺席报成了答案:dl_iterate_phdr 返回一个 dlpi_phnum == 0 的对象。 调用方分不出这个和「程序确实没有段」,于是断定没什么可找,而不是断定它没被告知。 要紧的调用方是 unwinder。它在段里找 PT_GNU_EH_FRAME,一个段都没有,于是一条 帧描述也找不到。实测后果,在一个会抛的程序里: libc++abi: terminating due to uncaught exception 没有任何诊断指出原因,而 _Unwind_Backtrace 走 0 帧 —— 与此同时编译、链接、 以及所有不抛的路径全绿。 头还在,丢的只是指向它的指针。这个包用到的每个链接器都在 ELF 头处定义 __ehdr_start,那正是辅助向量本来会报的东西。弱引用它,使得没有定义它的链接 回落到 musl 的答案而不是失败。 本文件是给 ELF 的,另外两种格式排除它,和它们原本排除 musl 的那份同理。 * ci: 那条交叉测量进 CI,而不是留成一个没人跑的脚本 它支持的断言是「给那个系统的程序可以被构建、被链接,而不需要那个系统提供的 任何文件」—— 这是一条对这个 port 的改动可以悄悄推翻的断言。 两个数字被断言:间接符号 0 个,那个系统供给的名字恰好 2 个。两个架构各跑一遍。 * fix: 探针问的两个问题是两个,答了第一个不等于答了第二个 「这个 port 调用了那个系统的哪些名字」与「这次链接需要哪些名字」不是一回事。 dyld_stub_binder 属于后者:没有源文件写它,链接器为自己的惰性绑定发出引用, 而它是否出现取决于链接器的版本。 于是枚举把它滤掉(它不是那个问题的答案),链接那一步照常包含 stub —— 那一步 才是「stub 够不够」的判据。 实测 2026-08-22:我把第一个问题的答案当成了第二个的,本机(lld 22)绿而 CI (lld 18)红。 * ci: 链接不是运行,于是把制品带到那个系统上启动它 此前这个仓库能说的只是「给那个系统的程序可以在这里被链接」。那和「它能跑」 是两个断言,而第二个在这台机器上问不出来:那个系统的 64 位 ARM 变体拒绝启动 没有签名的镜像,而签名只能在那个系统的机器上做。 于是 tools/cross-build-macos.sh 在 Linux 上产出制品,工作流把它传到 macos-14, 在那里 ad hoc 签名(身份是 `-`,它不声称是谁做的,而这正是那个系统启动它 所要求的)并运行。 examples/cross-hello 刻意不是一个 hello:打印常量只能证明控制到达了第一条 语句。它检查的是 C 库起来了 —— 收到了自己的名字、分配可用且整块可写、 文件能建能写能读回。 ⇒ 这是承重实验 E。此前它在设计文档里是「未做」。 * test: 一条覆盖三个操作的断言,失败时一个都不点名 实验 E 的第一次运行:交叉产出的程序在那个系统的真机上启动了,argv、分配、 建文件、写文件全过,而「读回」那一条失败 —— 而它同时断言了 fopen、fscanf 和值相等三件事,所以它没说是哪一件。这是在一台我跑不了的机器上,唯一的报告。 拆成四条,并把 fscanf 的返回值打出来。 顺带修交叉脚本的一处真缺陷:它的排除表照的是 Linux 的那份,漏了那个系统自己的 —— okm_phdr.c 从 ELF 头回答 dl_iterate_phdr,而那个格式没有 ELF 头。 * feat: 为没有操作系统的机器构建 —— riscv64 此前 openkal-musl 建不了裸机目标,而挡路的不是 libc++ 的任何性质:vendored 的 musl 树被裁到 aarch64/generic/x86_64,而 musl 的每个架构都要自己的 bits/alltypes.h 与 bits/syscall.h,riscv64 的源材料就不在树里。 引入 musl/arch/riscv64(取自这棵树其余部分所来自的同一个 musl 1.2.5 发布), 按 musl-generated/README.md 记的三条命令生成它的头。 随后暴露的三处 port 层缺口,每一处都被点名而不是被绕过: 1. okm_setjmp.S 只有 x86_64 与 aarch64 两支。补 riscv64 —— 从 musl 自己的 src/setjmp/riscv64 转写,寄存器是那个架构过程调用标准的被调用者保存集, 浮点那一半按 ABI 而不是按硬件条件化(软浮点 ABI 的程序没有那些寄存器)。 2. SYS_renameat 在那个架构上不存在。内核的表长大了,而在长大之后新增的架构 没有更早那个操作的号 —— 所以这一对写成两个条件,而不是一个名字加一个可选的 第二个。 3. port/include/pthread_arch.h 的 MC_PC 只分 aarch64 和「其余」,而那个架构 把数组拼成两个前导下划线。补一支,名一个存在的成员 —— 那正是那段注释说的 理由。 实测:mcpp build --target riscv64-none-elf 产出 1359 个对象,抽验为 「ELF 64-bit LSB relocatable, UCB RISC-V, RVC, double-float ABI」。 宿主目标不受影响,examples/posix 仍 0 failures。 ⚠️ cfg(os = "none") 的实现依赖已声明但还解析不了:openkal-opensbi 的 main 用版本号依赖 openkal,与这条线的 git 依赖冲突,而那个仓库本轮推不上去(403)。 本地用 path 验证了整条链能建。 * test: 缓冲的报告会谎报程序停在哪 输出到管道是全缓冲的,所以中途死掉的程序会丢掉它已经打印的东西 —— 于是跑它的 那台机器报的不是程序走到了哪。实测:一次运行只显示一行就停,而那一行不是它 执行过的最后一行。 一个报告被缓冲的判据,是一个会谎报自己停在哪的判据。关掉缓冲。 * fix: 判据自己有副作用,于是它测的是它自己 实验 E 的交叉产物在那个系统的真机上跑到第六项就死了,看起来像这个 port 的缺陷, 或者像交叉构建的缺陷。把同一份源码在这台机器上原生建一次 —— 它一样死在同一处, 这就说明两者都不是。 真因在程序自己的断言宏:它读 `(ok) ? "ok" : "FAIL"' 之后又读 `if (!(ok))', 所以带副作用的参数发生了两次,而下面一条参数是 `fclose(f) == 0'。第二次关的是 一个已经关掉的流,程序死在紧接着的那次 fopen。 修:条件求值一次。修后八项全过。 一个条件里带副作用的判据,是一个连自己一起测的判据。 同时把「原生构建同一份源码」作为对照组接进 CI —— 它刚刚证明了自己的价值, 而且两个长得一样的失败,靠把同一份源码建两遍来分开,便宜到可以每次都做。 * ci: 对照组放错了 job —— 那个 job 没有 mcpp cross-macos 只装 clang/lld/llvm,它是刻意不装构建工具的:它要证明的正是 「不需要那个系统提供的任何文件」。原生对照要 mcpp,所以它属于已经装了 mcpp 的 那个矩阵 job,而且放在那里还顺带在四个系统上各跑一遍,而不是只在一个。 * ci: 源文件挪进 src/ 之后,workflow 里那行没跟着改 * ci: 对照组也要点名 target —— 一个系统需要它 那一步没带 --target,于是在那个系统上为它的另一条工具链构建,链接时缺 kernel32 供给的名字。这个 job 里其它每一步都点名了它,这一步漏了。 * deps: 裸机的实现依赖指到这条线上 openkal-opensbi 的分支此前不存在(那个仓库的 origin 走 HTTPS,推不上去), 所以这条依赖临时指着 main —— 而 main 用版本号依赖 openkal,与这条线的 git 依赖 冲突,riscv64 的解析因此断在那里。 分支推上去之后指过来。实测:纯 git 依赖解析,1359 个对象,真 RISC-V ELF。 * feat(port): C 库不该带着它下面没有的接口一起被编出来 裸机上一个正文只有 `import std;` 和一行输出的程序,链接失败在十五个它永远走不到 的名字上 —— kal_fs_close_file、kal_task_yield、kal_process_spawn…… 不是缺陷,是规范在按设计工作:openkal 6.2 禁止实现提供「在场但总是失败」的接口, 所以只提供 core 的实现就是不提供 fs/process/task。而 C 库调它们 —— 只要它**被 编成**调它们,链接就失败,对**每一个**程序,因为 fclose 在任何用了 stdio 的程序 的退出路径上,而描述符表要关掉它持有的东西。 ## ⭐ 于是 C 库要被配置,而 openkal 实现不被配置。这看起来矛盾,不是。 openkal 实现不能回答「不支持」,因为 openkal 的面上没有这个答案,调用者拿它 做不了任何事; C 库可以,因为 POSIX 的面上**有** —— ENOSYS —— 而且每个 open 的调用者本来就 要处理失败。 一层用「不存在」表达的缺席,到上一层变成用「一个有定义的错误」表达的缺席。这是 两层各自在做自己的事。会出错的地方是「什么都没做却报成功」,而下面没有这种。 ## 一个缝,不是四十个 #if fs/process 在 okm_syscall.c 的 switch 里被调用约四十处。每处加守卫 = 把一个决定 放在四十个地方,而第四十一处会是漏掉的那个 —— 并且是**悄悄**漏掉,因为它显形的 方式是某个当时没人在构建的目标上的链接失败。 所以决定只做一次:port/src/okm_opt.h。调用方一律写 okm_fs_* / okm_process_* / okm_task_*。接口在场时它们**就是**接口(宏,零代码);不在场时它们是答案。 okm.h 只说环境有什么,okm_opt.h 说因此如何 —— 输入和后果分开。 ## 每个答案都是照实的,不是桩 - okm_fs_* / okm_process_spawn → not_supported ⇒ ENOSYS - okm_task_start → not_supported ⇒ __clone 现有路径给出 **EAGAIN**,而不是 ENOSYS。⭐ 被重建的那张面决定答案,不是一条规则套到底:pthread_create 规定了 EAGAIN 表示「上下文的资源不可用」,而它**没有**「本系统没有上下文」这个值。 - ⚠️ okm_task_wait → ENOSYS 而**不是** again。musl 写成 `while (*addr == val) futex(WAIT)`,单上下文下没人能改 *addr,回答「值没匹配, 再看一眼」会让它**无声地**永远转下去。 - okm_task_wake → 成功,woken=0。唤醒零个等待者是完成了,不是拒绝。 - close/join → 无操作。释放一个不可能拿到的句柄,join 一个从没启动的上下文。 ## ⚠️ 目标是实现的代理,而且是不完美的代理 清单在 cfg(os="none") 下清掉三个宏。板子**可以**带真的文件系统,所以三个宏都是 `#ifndef` 守卫的,有的工程从自己的构建里打开,不用改这个包。mcpp 按目标而不是按 「谁满足了 capability」求值;等它能的时候,要改的只有 okm.h 那一处。 已验证:宿主 posix 探针 24 条断言 0 failures(线程/spawn/文件/时钟全在内); 裸机 riscv64 全部 2718 个对象里,kal_fs_* / kal_process_* / kal_task_* 的未定义 引用为 **0**。 * fix(port): 判据用错了「语言」,而真正的性质是「谁在编译」 port/include/features.h 里那道守卫是 `#ifdef __cplusplus`,旁边写着理由: musl 自己的源码是 C,而且从不被当作别的语言编译。所以一个 C++ 翻译单元 必然**不是**它们中的一个。 两句都对。从它们推出的结论不对,因为它被反着用了:C++ 单元当然不是 musl 的, 但 C 单元不见得就是 musl 的。 实测 2026-08-23,给裸机目标编 compiler-rt 的软浮点例程时 —— 是 C,不是 musl, 第一个文件就停在: int_util.c:49: use of undeclared identifier `__weak__' features.h:6: expanded from macro `weak' ⇒ 要紧的性质不是单元的**语言**,是**这个包是不是正在编译它的那个包** —— 而这 件事这个包可以直接说出来:mcpp.toml 给自己的构建定义 OKM_MUSL_INTERNAL,而 `defines` 不会传到消费者(publicUsage 带的是 include 目录和 flags,这个都不是)。 两边都建过验证了。 于是消费者在**两种语言里**都拿到中和的那一份,musl 自己的源码拿到文件末尾那套 weak_alias 机制。C++ 那边 hidden 仍然给 C 链接(不是清空 —— 清空会编过然后 链不上),C 那边清空。 ⚠️ 顺带记下一条 mcpp 缺陷,因为它一度把我引向错误的结论:在这里加一个 flag,对 「按 path 依赖这个包的例子」**没有任何效果** —— 重建没用,touch 源码也没用, 生成的构建文件里还是上一份清单。删掉 target/ 和 mcpp.lock 立刻就对了。 所以**路径依赖的清单变更不会让消费者的计划失效**,而它显形的方式是「改了像没改」。 我据此一度断言 `[build] defines` 到不了路径依赖 —— 那是错的,已改回 defines。 * fix(port): cache.c 的 vDSO 依赖用错了守卫,而 riscv64 才第一次完整链接 openkal 之上没有 vDSO —— 那是某个系统供给它启动的程序的设施,而这里没有东西是 它启动的。musl 自己的安排本来是对的:__vdsosym 只在 VDSO_USEFUL 下定义,这个 port 的 syscall_arch.h 替换掉了架构那份、因此从不定义它,而 __vdsosym 的每个 使用者都在一个同样不会被定义的 VDSO_* 守卫后面。所以 vdso.c 编成空对象,没人 少了它。 ⚠️ 除了一个。cache.c 把它的 vDSO 使用守在 SYS_riscv_flush_icache 上 —— 一个 **系统调用号**,而生成的 bits/syscall.h 确实定义了它 —— 而不是守在 VDSO_* 上。 于是在 riscv64 上只有它编出了一个对「已经被编掉的定义」的调用: ld.lld: error: undefined hidden symbol: __vdsosym ⚠️ 它到今天才显形,不是因为它新:在别的架构上那个守卫为假、文件是空的,而 riscv64 的**第一次完整链接**就是这一次。一条 port 的潜伏缺陷,被一个没有任何 退路的目标挖出来 —— 这正是 §8.2 把裸机定为验收环境的理由。 已验证:宿主 posix 探针仍然 0 failures。 * deps: 裸机上也要有东西接收控制 openkal-opensbi 加上 features = ["standalone"],和 linux/windows 两条一致: C 库知道程序自己不带运行时,所以由它声明。 * fix(tools): 交叉脚本里的 flag 清单是清单的第二份拷贝,而它第一次就掉队了 crypt_r.c:23: type specifier missing | weak_alias(__crypt_r, crypt_r); 两个交叉脚本各自维护一份 cflags,而它们自己的注释就写着「保持和 mcpp.toml 同步,是让第二问的答案是**已配置的那个**答案的原因」。 ⭐ 它在清单第一次多出一个 flag 时就掉队了 —— 而说出这件事的是 CI。 一个决定放在两个地方,这是另外那个地方。 补上 -DOKM_MUSL_INTERNAL=1(features.h 靠它决定 weak/hidden/weak_alias 是否 有意义),以及 cache.c 的排除(它把 vDSO 使用守在系统调用号上,见上一条)。 已本机验证(用载荷里的 llvm@20.1.7):1354 个对象,indirect symbols = 0, 借的名字恰好两个,链接通过 —— probe: ok * feat: Apple 的 int64_t 拼成 long long,而 musl 的 aarch64 那份写着 long musl 用三行 bits/alltypes.h 陈述它的数据模型。Windows 需要一份变体,因为它是 LLP64 而 musl 的每个架构都是 LP64。Apple **是** LP64 —— 一个 long 装得下指针 —— 却仍然在三行里的一行上不同:`int64_t` 在那里拼成 `long long`,在 Linux 上拼成 `long`。 ⚠️ 一处拼写,而它不是装饰。实测 2026-08-23,从 Linux 交叉构建到 arm64-apple-macos: okm_syscall.c:439: incompatible pointer types passing 'uint64_t *' (aka 'unsigned long *') to parameter of type 'kal_u64 *' (aka 'unsigned long long *') `kal_u64` 来自**编译器**(__UINT64_TYPE__),它对目标 ABI 的陈述是权威的; `uint64_t` 来自这三行,而它们带的是 Linux 的答案。同样的宽度,不同的类型, 两者不转换。 aarch64 的两行清单改成互斥的一对,和 x86_64 早就有的 Windows 那对同一形状。 * fix: struct stat 的字段拼法是 C 库的事,不是 OS 的事;并去掉宿主链接器覆盖 ## sys/stat.h 覆盖 POSIX 标准化了 st_mtim/st_atim,musl 提供它们;Apple 的平台早于标准化,用 st_mtimespec/st_atimespec,而按 __APPLE__ 分派的消费者去够第二组。实测交叉构建 libc++ 到 arm64-apple-macos: time_utils.h:277: no member named 'st_mtimespec' in 'stat' ⚠️ 是**别名**不是第二组字段:只有一个 struct stat、一个布局 —— musl 的 —— 这里 只是给它的两个成员第二个名字。写哪个拼法都到同一批字节。 ⇒ 另一条路是给这个目标关掉 <filesystem>(是个 __config_site 开关,也能work), 那是为了绕开一处拼写而拿掉一个能用的设施。 ## macOS 的 --ld-path 去掉 它点名这个系统自己的链接器,为的是绕开这个包**最初**被构建时那个编译器产生的 构造。上面那段重测就是它能走的全部理由:用开源工具链时同样的对象产生**零个** indirect symbol,ld64.lld 链得上。 ⚠️ 而它**必须**走,因为 mcpp 的条件在**目标**上而不在宿主上。cfg(os = "macos") 在「在 macOS 上构建」和「为 macOS 构建」时都为真,而第二种情况下机器是 Linux, 那里的 /usr/bin/ld 是 GNU ld,链不了 Mach-O: /usr/bin/ld: Error: unable to disambiguate: -dead_strip * fix(x86_64-windows): __LONG_MAX 还留着 LP64 的答案 这一档的数据模型改了一半:_Addr / _Int64 / _Reg 都已经是 long long,唯独 __LONG_MAX 是从 x86_64 档整份抄过来的 0x7fffffffffffffffL —— 而 Windows 是 LLP64,long 是 32 位。 ⚠️ 同一个事实在这份文件里说了两遍,只改了一遍。 实测(Linux → x86_64-windows-gnu,libcxx/src/charconv.cpp): libc/src/__support/CPP/limits.h:55: error: non-type template argument evaluates to 9223372036854775807, which cannot be narrowed to type 'long' musl/include/limits.h:29: note: expanded from macro 'LONG_MAX' musl-generated/x86_64-windows/bits/alltypes.h:6: #define __LONG_MAX 0x7fff… ⇒ 这正是 §1.3 说的那一层:C 库是唯一必须知道目标 ABI 的地方,因为它重建的是 POSIX,而 POSIX 自己命名了 long。openkal 接口上没有这个问题。 连带修好 ULONG_MAX —— musl 把它写成 (2UL*LONG_MAX+1),跟着 LONG_MAX 走。 * fix(windows): 链接线上的 -lgcc/-lgcc_eh 是 GCC 的运行时,而编译器是 clang ⚠️ 它们只在装了 mingw 的机器上成立。干净 CI runner 上: lld: error: unable to find library -lgcc lld: error: unable to find library -lgcc_eh ⭐ 而上面的 macOS 块两个都不写,是同一个 C 库、同一个编译器 —— builtins 由图 供给(openkal-llvm-runtime 为目标编 compiler-rt),thread-local 的那个 helper 是 同一份构建里的 __emutls_get_address。这个块是唯一的例外。 原注释的两句话都是对的(-lgcc 提供编译器发出调用而 C 库不定义的例程;-lgcc_eh 提供这个格式特有的 thread-local helper)。错的是「该由这个 port 来点名它们」。 ⇒ 实测移除后,图里有 openkal-llvm-runtime(现在为这个格式编 compiler-rt 的 builtins 和 emutls.c)时,examples/same-source 链接成功、程序跑起来并打印 unwound: true。 ⚠️ Linux 那条的 -lgcc 保留:它解析到载荷自带的那份,确实在场,且那条路径是绿的。 * fix(windows): __main —— 编译器发的那个调用,而这个 C 库已经让它没事可做 PE 上编译器把 `call __main` 作为 main 做的第一件事发出来。在那个名字来自的 安排里,这个调用负责跑全局构造器 —— C 运行时的启动不跑,责任落在 main 顶上。 而这个 C 库的启动跑。__libc_start_main 在调用 main 之前遍历 .init_array,在每个 格式上都如此。所以构造器恰好在 main 之前跑一次,__main 没事可做。 ⚠️ 但它不能干脆缺席:那个调用在对象里,定不定义它都在 ⇒ ld.lld: error: undefined symbol: __main ⚠️ 它也不能自己再跑一遍构造器 —— 那会让每个构造器跑两次,而那不是链接器能报的 失败。 ⚠️ 放在 port/src/win/ 而不是挨着邻居:port/src/*.c 是包级 glob,放在旁边会被每个 目标编到,而 __main 只有这个格式的编译器会发调用。用目录布局表达这条限制,而不是 靠一条要跟包含项争胜负的排除项。 ── 以及一条对上一提交的更正 ───────────────────────────── 上一提交说「实测移除 -lgcc/-lgcc_eh 后 same-source 链接成功并跑起来」。⚠️ 那次 实测是假绿:清空 ~/.mcpp/git 重来之后,链接报 __main 和 ___chkstk_ms 两个未定义。 而这份清单自己第 114 行就写着原因 —— 「改 path 依赖的 MANIFEST 不会让消费者的 plan 失效」。我改的正是一个依赖的 manifest,而消费者没有重新链接。 ⇒ 结论不变(-lgcc 是 GCC 的运行时,不该出现在 clang 的链接线上),但两个符号 要各自有正当来源:__main 在这里,___chkstk_ms 从 compiler-rt 取(openkal-llvm- runtime 那侧)。 * fix(windows): -lgcc 只在编译器真叫这个名字时才写 上一提交把它从清单里整条拿掉了,而那对另一种配置是错的: 无条件写 lld: error: unable to find library -lgcc (干净 runner,clang,交叉到 PE) 从不写 undefined reference to `___chkstk_ms' undefined reference to `__mulxc3' (Windows runner,GCC,图里没有 C++ 运行时来供给 compiler-rt) 两次各自都是另一种配置下的正确行为。 ⚠️ 清单说不出这件事,所以有了 build.mcpp:[target.'cfg(windows)'] 描述的是**目标**, 而这是关于**解析到哪个工具链**的问题。mcpp::compiler() 现在能回答它。 ⚠️ 只在 PE 上,并且带上 gcc_eh:这个格式上 thread-local 变量通过一个 helper 到达 而不是通过处理器给的地址,那个 helper 在第二个归档里。图里有 openkal-llvm-runtime 时,compiler-rt 的 emutls.c 是同一个答案,两个归档都不会被查。 ⚠️ ELF 和 Mach-O 上不写:那里驱动即使在 -nostdlib 下仍会链接编译器的运行时, 或者由图供给;在这里加一个名字会让已经有来源的东西多一个来源。 本机复验:同一个 examples/posix,clang 工具链下链接线无 -lgcc 且程序能跑; gcc@16.1.0 工具链下 -lgcc/-lgcc_eh 回到链接线上并构建成功。 * fix: 同上 —— 构建程序改读环境变量,缺席按推断而不是默认值处理 mcpp::compiler() 是被运行这个构建程序的工具编译的,而 CI 装的是已发布版: build.mcpp:37: error: no member named 'compiler' in namespace 'mcpp' ⚠️ 而为它写的兜底 mcpp::toolchain_dir() 同样不在那个版本里 —— 兜底和被兜的东西 一起失败了。 ⇒ 读 MCPP_COMPILER。缺席时答 "gcc":clang-over-openkal 交叉到 PE 与这个变量同一个 发布,所以一个传不了它的工具也做不出那种配置,而它能做出的只有 GCC 那条。 * fix(macos): 这个格式上「把库带起来」和「被进入」不是同一个时刻 ⚠️⚠️ 在 ELF 上顺序是:库 → 构造器 → main。Mach-O 把它倒过来 —— 动态加载器在把 控制权交给入口**之前**跑镜像的构造器,于是 C++ 运行时的静态初始化器在这个库还 完全没初始化的时候执行。 实测 2026-08-23,一个在 Linux 上构建、在**真 Mac** 上运行的 arm64 镜像: dyld: running initializer 0x…f3aa48 in openkal-same-source _GLOBAL__I_000100 → std::ios_base::Init::Init() → DoIOSInit::DoIOSInit() → init_stream<basic_istream, __stdinbuf<char>>(FILE*, …) → __stdinbuf<char>::__stdinbuf(FILE*, mbstate_t*) ← EXC_BAD_ACCESS libc++ 在构造它的标准流,流建在这个库的 stdin 上,而那一刻 okm_start 还没跑。 ⇒ 把「带起来」提成 __okm_libc_init(),带一次性守卫;需要更早的那个平台更早地 调它(port/src/mach/early_init.c 的一个构造器)。调两次不是危险,调晚了才是。 ⚠️ 用普通标志而不是锁:这段代码跑在线程层存在之前 —— __init_tls 正是它做的事情 之一 —— 所以这里用互斥量等于在用它正在安装的东西。而此刻每个格式上都恰好只有一个 执行上下文:加载器没起过,我们也没有。 ⚠️ 构造器之间的顺序是**链接顺序**,不是优先级参数(那个在这个格式上只在单个翻译 单元内有效)。成立的是:本包是 C++ 运行时的**依赖**,所以它的对象排在那个运行时 的对象之前。⭐ 而真正让它安全的是那个守卫而不是顺序 —— 一个依赖顺序正确才成立的 修法,是一个在顺序不对时静默失效的修法。 ⚠️ 文件放在 port/src/mach/:port/src/*.c 是包级 glob,放在旁边会被每个目标编到。 * fix(linux): -lgcc 也只在编译器叫这个名字时才写 ld.lld: error: unable to find library -lgcc (macOS 宿主交叉到 x86_64-linux-gnu) 那台机器上没有给 Linux 目标的 libgcc,也没有理由有:编译器是 clang,它的运行时是 compiler-rt。这个 flag 一直只在宿主和目标碰巧一致时才成立。 ⇒ 与 PE 那一条同一条规则,只是晚一步被发现 —— 因为宿主维度直到现在才被测。 * feat: declare the c-abi layer this package supplies `mcpp:c-abi=musl` states that this package is the C library — the triple's environment field — and that it answers to the name `musl`, which is the same answer it would give above a kernel rather than above openkal. What differs is not the library a program sees but what that library is written on. Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD * chore: 0.3.2 The manifest states a capability the previous release did not, and the index serves published tarballs rather than branches. A consumer resolving through the index would otherwise get content in which this package declares no layer, and the engine would resolve its target side to the compiler payload. Claude-Session: https://claude.ai/code/session_01Q4ucduLuSsETHYJ1RkGVVD --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent 83c9bfe commit 6108d34

47 files changed

Lines changed: 4353 additions & 86 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,31 @@ jobs:
152152
grep -qE '^-- failures: 0 --$' run.log
153153
! grep -q '^FAIL:' run.log
154154
155+
- name: The same program, built the ordinary way, as a control
156+
run: |
157+
# ⚠️ THIS STEP EXISTS BECAUSE IT ALREADY EARNED ITS PLACE.
158+
#
159+
# The cross-built copy failed on a machine of the other system, and the
160+
# obvious reading was a defect in this port or in the way it was
161+
# cross-built. A native build of the SAME source reproduced it here ---
162+
# which said it was neither, and the defect was in the program's own
163+
# assertion macro, which evaluated its condition twice and so closed a
164+
# stream that was already closed.
165+
#
166+
# Two failures that look alike are told apart by building the same
167+
# source two ways, and that is cheap enough to do every time.
168+
# ⚠️ The target has to be named here as it is everywhere else in this
169+
# job: one system needs it, and a step that omitted it built for that
170+
# system's other toolchain and failed at the link with names kernel32
171+
# supplies. Measured, on the first run of this step.
172+
extra=''
173+
[ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}'
174+
cd examples/cross-hello
175+
mcpp build $extra
176+
binary="$(find target -type f \( -name 'cross-hello' -o -name 'cross-hello.exe' \) | head -1)"
177+
"./$binary" 2>&1 | tee out.log
178+
grep -q -- '-- failures: 0 --' out.log
179+
155180
- name: The counts agree with the system's own
156181
working-directory: examples/wordcount
157182
run: |
@@ -171,3 +196,99 @@ jobs:
171196
172197
# The variable was consulted, and the interval was measured.
173198
grep -q 'elapsed .* nanoseconds' run.log
199+
200+
# ---------------------------------------------------------------------------
201+
# The measurement musl/PATCHES.md records, made from a machine that is not the
202+
# system it is about.
203+
#
204+
# It is a job rather than a note because the claim it supports --- that a
205+
# program for that system can be built, and linked, without any file that
206+
# system supplies --- is one that a change to this port could silently undo.
207+
# The two numbers it asserts are zero indirect symbols and exactly two names.
208+
cross-macos:
209+
name: cross-link for the other system, from this one
210+
runs-on: ubuntu-24.04
211+
timeout-minutes: 30
212+
steps:
213+
- uses: actions/checkout@v4
214+
215+
- name: The implementation for that system
216+
run: |
217+
branch='${{ github.head_ref || github.ref_name }}'
218+
git clone --quiet https://github.com/mcpplibs/openkal-macos.git ../openkal-macos
219+
git -C ../openkal-macos checkout --quiet "origin/$branch" 2>/dev/null || true
220+
git clone --quiet https://github.com/mcpplibs/openkal.git ../openkal
221+
git -C ../openkal checkout --quiet "origin/$branch" 2>/dev/null || true
222+
223+
- name: Install LLVM
224+
run: |
225+
sudo apt-get update -qq
226+
sudo apt-get install -y -qq clang lld llvm
227+
# ld64.lld is the Mach-O driver of the same binary; Ubuntu's package
228+
# installs it under a version suffix and without the plain name.
229+
v=$(ls /usr/lib/llvm-*/bin/ld64.lld 2>/dev/null | sort -V | tail -1)
230+
test -n "$v" || { echo "::error::this image has no ld64.lld"; exit 1; }
231+
d=$(dirname "$v")
232+
echo "LD64=$v" >> "$GITHUB_ENV"
233+
# The tools are installed under a version suffix and the probe asks for
234+
# them by their plain names, so the directory that has both spellings
235+
# is named rather than assumed.
236+
echo "NM=$d/llvm-nm" >> "$GITHUB_ENV"
237+
echo "$d" >> "$GITHUB_PATH"
238+
"$d/llvm-nm" --version | head -1
239+
clang --version | head -1
240+
241+
- name: Zero indirect symbols, and two names
242+
run: |
243+
bash tools/probe-cross-macos.sh aarch64
244+
bash tools/probe-cross-macos.sh x86_64
245+
246+
# ⚠️ LINKING IS NOT RUNNING, AND THIS IS WHERE THAT IS ADMITTED.
247+
#
248+
# Everything above happens on a machine that is not the system in
249+
# question, and everything above is about the objects and the link. Whether
250+
# the result STARTS is a different question, and it cannot be asked here:
251+
# that system's 64-bit ARM variant refuses to start an image that is not
252+
# signed, and the signature can only be made on a machine of that system.
253+
#
254+
# So the artifact is carried there. The job that receives it is the only
255+
# place in this workflow where the claim becomes "a program built here
256+
# runs there" rather than "a program built here links".
257+
- name: A program for that system, built here
258+
run: |
259+
bash tools/cross-build-macos.sh aarch64 examples/cross-hello/src/main.c cross-hello-arm64
260+
file cross-hello-arm64
261+
262+
- uses: actions/upload-artifact@v4
263+
with:
264+
name: cross-hello-arm64
265+
path: cross-hello-arm64
266+
retention-days: 1
267+
268+
# ---------------------------------------------------------------------------
269+
# The other half of the claim, on a machine of that system.
270+
cross-macos-run:
271+
name: what was cross-built starts, on that system
272+
needs: cross-macos
273+
runs-on: macos-14
274+
timeout-minutes: 15
275+
steps:
276+
- uses: actions/checkout@v4
277+
- uses: actions/download-artifact@v4
278+
with:
279+
name: cross-hello-arm64
280+
281+
- name: Sign it, because this system will not start it otherwise
282+
run: |
283+
chmod +x cross-hello-arm64
284+
# Ad hoc: the identity is `-', which asserts nothing about who made
285+
# the program and is exactly what this system requires before it will
286+
# start one on this architecture. A program that is distributed needs
287+
# more; a program that is checked needs this.
288+
codesign -s - --force cross-hello-arm64
289+
codesign -dv cross-hello-arm64 2>&1 | head -3
290+
291+
- name: It starts, and the C library beneath it came up
292+
run: |
293+
./cross-hello-arm64 | tee run.log
294+
grep -q -- '-- failures: 0 --' run.log

build.mcpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import mcpp;
2+
import std;
3+
4+
// ⭐⭐ THE COMPILER'S OWN RUNTIME, NAMED ONLY WHEN IT HAS THAT NAME.
5+
//
6+
// A compiler emits calls to routines no C library defines — a 128-bit shift, a
7+
// stack probe, a complex multiply on `long double`. They live in `libgcc.a`
8+
// under GCC and in compiler-rt under clang, and the two are not
9+
// interchangeable names for one file: `-lgcc` on a clang link is a library that
10+
// does not exist.
11+
//
12+
// ⚠️ THE MANIFEST CANNOT SAY THIS, WHICH IS WHY THERE IS A PROGRAM. A
13+
// `[target.'cfg(windows)']` block describes the TARGET, and this is a question
14+
// about the toolchain that resolved. Both measurements arrived on 2026-08-22
15+
// from the same missing distinction:
16+
//
17+
// naming it unconditionally lld: error: unable to find library -lgcc
18+
// (a clean runner, clang, cross to PE)
19+
// naming it never undefined reference to `___chkstk_ms'
20+
// undefined reference to `__mulxc3'
21+
// (a Windows runner, GCC, no C++ runtime in the
22+
// graph to supply compiler-rt instead)
23+
//
24+
// Each was the correct behaviour for the other configuration.
25+
//
26+
// ⚠️ PE ONLY, AND `gcc_eh` WITH IT. On this format a thread-local variable is
27+
// reached through a helper rather than through an address the processor
28+
// supplies, and that helper is in the second archive rather than the first.
29+
// Where the graph brings `openkal-llvm-runtime`, compiler-rt's `emutls.c` is
30+
// the same answer and neither archive is consulted.
31+
//
32+
// ⚠️ NOT ON ELF OR Mach-O. There the driver still links the compiler's runtime
33+
// even under `-nostdlib`, or the graph supplies it; adding a name here would be
34+
// a second source for something that already has one.
35+
36+
// ⭐⭐ WHICH COMPILER FAMILY RESOLVED — AND WHAT AN ABSENT ANSWER MEANS.
37+
//
38+
// ⚠️ READ FROM THE ENVIRONMENT, NOT THROUGH A HELPER. A build program is
39+
// compiled against the `mcpp` module of whichever tool RUNS it, so naming a
40+
// helper newer than the released tool makes this package require an unreleased
41+
// one. Measured 2026-08-22, on every row of three repositories at once:
42+
//
43+
// build.mcpp: error: no member named 'compiler' in namespace 'mcpp'
44+
// build.mcpp: error: 'toolchain_dir' is not a member of 'mcpp'
45+
//
46+
// — and the second is the more useful of the two, because `toolchain_dir` was
47+
// the FALLBACK written for the first. The environment variables are the
48+
// contract and `std::getenv` reads them on every version.
49+
//
50+
// ⚠️ AND AN ABSENT VALUE IS NOT "NO COMPILER". Measured against mcpp 2026.8.19.1,
51+
// which is what CI installs: `MCPP_TARGET_OS`, `MCPP_HOST`, `MCPP_OUT_DIR`,
52+
// `MCPP_MANIFEST_DIR`, `MCPP_TARGET` and `MCPP_TARGET_ARCH` are all set, and
53+
// `MCPP_COMPILER` and `MCPP_TOOLCHAIN_DIR` are absent.
54+
//
55+
// ⭐ So absent means "a build tool from before the question could be asked" —
56+
// and such a tool cannot produce the configuration the answer would change. The
57+
// clang-over-openkal cross to PE arrives on the same release as the variable.
58+
// The one configuration a tool that predates it can produce is the GCC one, so
59+
// that is what absent is answered with, and it is a deduction rather than a
60+
// default.
61+
std::string compiler_family() {
62+
if (const char* v = std::getenv("MCPP_COMPILER"); v && *v) return v;
63+
return "gcc";
64+
}
65+
66+
std::string env_or_empty(const char* name) {
67+
const char* v = std::getenv(name);
68+
return v ? v : "";
69+
}
70+
71+
int main() {
72+
const std::string os = env_or_empty("MCPP_TARGET_OS");
73+
const std::string fam = compiler_family();
74+
75+
if (fam != "gcc") return 0;
76+
77+
// ⚠️ ELF NEEDS IT TOO, AND FOR THE SAME REASON THE PE BLOCK DID. The Linux
78+
// block of the manifest named `-lgcc` unconditionally, and that resolved on
79+
// a Linux host because a payload with it happens to be installed there.
80+
// Measured 2026-08-23, a macOS host cross-building for `x86_64-linux-gnu`:
81+
//
82+
// ld.lld: error: unable to find library -lgcc
83+
//
84+
// ⇒ Under clang the answer is compiler-rt, which `openkal-llvm-runtime`
85+
// builds for this target as it already did for the other three.
86+
if (os == "linux") {
87+
mcpp::link_lib("gcc");
88+
} else if (os == "windows") {
89+
mcpp::link_lib("gcc");
90+
mcpp::link_lib("gcc_eh");
91+
}
92+
return 0;
93+
}

examples/cross-hello/mcpp.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# The same program, built the ordinary way.
2+
#
3+
# It exists as a control. The cross-built copy of this source is carried to a
4+
# machine of that system and run there; if it fails, the failure is either in
5+
# the port or in the way it was cross-built, and those are different things. A
6+
# native build of the same source on the same machine separates them --- which
7+
# is the step that, on the first failure of the C++ runtime, turned a day of
8+
# guessing into four measurements.
9+
[package]
10+
name = "cross-hello"
11+
version = "0.1.0"
12+
13+
[dependencies]
14+
openkal-musl = { path = "../.." }
15+
16+
[targets.cross-hello]
17+
kind = "bin"
18+
main = "src/main.c"
19+
20+
[build]
21+
cxx_runtime = "host-coupled"

examples/cross-hello/src/main.c

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* A program built on one system and started on another.
2+
*
3+
* It is deliberately not a "hello": what it prints has to be something the
4+
* machine that runs it can check, and what is worth checking is that the C
5+
* library came up --- that streams work, that the environment was received,
6+
* that allocation works. A program that printed a constant would prove only
7+
* that control reached its first statement. */
8+
#include <stdio.h>
9+
#include <stdlib.h>
10+
#include <string.h>
11+
12+
int main(int argc, char** argv)
13+
{
14+
/* ⚠️ UNBUFFERED, AND THAT IS PART OF THE PROBE.
15+
*
16+
* Output to a pipe is fully buffered, so a program that dies part way
17+
* through loses everything it had printed --- and what the machine that
18+
* runs this reports is then not where the program got to. Measured: one run
19+
* showed a single line and stopped, and the line it showed was not the last
20+
* one it had executed. A probe whose report is buffered is a probe that
21+
* lies about where it stopped. */
22+
setvbuf(stdout, NULL, _IONBF, 0);
23+
24+
int failures = 0;
25+
/* ⚠️ THE CONDITION IS EVALUATED ONCE, AND THE FIRST VERSION DID NOT.
26+
*
27+
* It read `(ok) ? "ok" : "FAIL"' and then `if (!(ok))', so an argument with
28+
* an effect happened twice --- and one of the arguments below is
29+
* `fclose(f) == 0'. The second close was of a stream already closed, and
30+
* the program died at the NEXT fopen with a segmentation fault.
31+
*
32+
* It was measured on a machine of the other system first, where it looked
33+
* like a defect in this port, and it reproduced on this one --- which is
34+
* what said it was not. A probe that has an effect in its own condition is
35+
* a probe that tests itself as well as its subject. */
36+
#define check(ok, what) do { \
37+
const int okm_ok_ = !!(ok); \
38+
printf("%s: %s\n", okm_ok_ ? "ok" : "FAIL", what); \
39+
if (!okm_ok_) ++failures; } while (0)
40+
41+
check(argc >= 1 && argv[0] != NULL, "the program received its own name");
42+
43+
char* p = malloc(4096);
44+
check(p != NULL, "an allocation succeeds");
45+
if (p) { memset(p, 0x5a, 4096); check(p[4095] == 0x5a, "the whole of it is writable"); free(p); }
46+
47+
FILE* f = fopen("cross-hello.tmp", "w");
48+
check(f != NULL, "a file is created");
49+
if (f) {
50+
check(fprintf(f, "%d", 12345) == 5, "it is written to");
51+
check(fclose(f) == 0, "it closes");
52+
f = fopen("cross-hello.tmp", "r");
53+
check(f != NULL, "it opens again for reading");
54+
if (f) {
55+
/* Split, because a single assertion over three operations names
56+
* none of them when it fails --- which is what the first version
57+
* of this program did, on a machine this one cannot run. */
58+
int v = 0;
59+
int n = fscanf(f, "%d", &v);
60+
printf(" (fscanf returned %d, value %d)\n", n, v);
61+
check(n == 1, "a formatted read succeeds");
62+
check(v == 12345, "it reads back what was written");
63+
fclose(f);
64+
}
65+
remove("cross-hello.tmp");
66+
}
67+
68+
printf("-- failures: %d --\n", failures);
69+
return failures != 0;
70+
}

0 commit comments

Comments
 (0)