2026-08-25 · 规范设计方案(待 review,尚未实施)
本文提出 mcpp 拥有自己的目标词表,而不是转述某个编译器的。 每一条设计都给出实测依据与拒绝的替代方案。
前置阅读:2026-08-25-os-toolchain-target-matrix.md(三个轴的实测矩阵)、
2026-08-25-target-system-analysis.md(现状缺陷)。
现有三套词表,互不一致,而 mcpp 三套都要打交道:
| 词表 | 例 | 谁读 |
|---|---|---|
| GCC / autoconf | x86_64-w64-mingw32 |
预构建载荷的编译器,以文件名的形式 |
| LLVM | x86_64-w64-windows-gnu |
clang --target= |
| mcpp | x86_64-windows-gnu |
目标表、输出目录、cfg()、打包 ABI tag |
⭐ 第三套已经存在了,只是它今天在转述 LLVM 而不是自己定义。 转述的代价实测可见:
| 平台 | env 命名 |
实测依据 |
|---|---|---|
| linux | C 库 | gnu→glibc,musl→musl |
| windows | 对象 ABI | gnu→_Z1fi,msvc→?f@@YAHH@Z |
| none | 对象格式 | elf |
同一段在三个平台上是三条不同的轴。这直接造成两处缺陷:
cfg(env = "gnu") 在两个平台上语义不同;
以及 Windows 上两个完全不同的 C 库共用 -gnu 一个名字
(MinGW CRT 与 musl,实测体积 587KB vs 9.8MB、依赖 msvcrt.dll vs 不依赖)。
实测 llvm 22.1.8:
clang++ --target=x86_64-pc-windows-musl -c t.cpp
#5 llvm::MCWinCOFFStreamer::emitCGProfileEntry(...) ← ICE,不是诊断
Windows 上四个非 MSVC 环境 gnu/cygnus/itanium/musl,前三个能编,
只有 musl 崩;预定义宏显示它从未被建模(无 __MINGW32__)。
⭐ 而 musl on Windows 真实存在——openkal-musl 就是它。
一个词表若拼不出已经存在的东西,它就不能作为 mcpp 的身份来源。
x86_64-w64-mingw32:mingw32 占据 OS 段,w64 占据 vendor 段。
这是 autoconf 的世界观(MinGW 是一个独立 OS),LLVM 已经拒绝了它并重拼为
windows-gnu。mcpp 更没有理由继承它。
<arch> - <os> - <libc>
没有 vendor 段。 实测 LLVM 那一段几乎总被填成 unknown,不承重;
苹果的 apple 与 MinGW 的 w64 是各自项目的标识,不是目标的性质。
mcpp 在映射时补出编译器需要的 vendor,自己不保存它。
⭐ 这是与 LLVM 的核心分歧,也是本方案的全部价值。
| mcpp 目标 | 第三段的含义 | 对应实现 |
|---|---|---|
x86_64-linux-gnu |
C 库 | glibc |
x86_64-linux-musl |
C 库 | musl |
x86_64-windows-gnu |
C 库 | MinGW CRT(封装 msvcrt.dll) |
x86_64-windows-musl |
C 库 | musl(openkal 之上) |
x86_64-windows-msvc |
C 库 | UCRT |
aarch64-macos |
(省略) | 由图决定:有 openkal 则 musl,否则 libSystem |
aarch64-macos-musl |
— | 词表里没有这一行,见 §6.1 |
riscv64-none |
(无) | 没有 C 库 |
Target aarch64-macos → arm64-apple-macos14.0
c-abi musl (openkal-musl@0.3.3, graph)
⭐ openkal 让 macOS 上也有了 musl,与 libSystem 并存 —— 与 Windows 完全同形。所以「该平台只有一个」这句话在 openkal 出现之前成立,现在不成立, 而我在同一份文档的 §5 里已经记下了这个实测却没有回头改这张表。
aarch64-macos 不带第三段时该解析成哪一个? 与 Linux/Windows 一致:
不作请求,由图决定 —— 有 openkal 依赖就是 musl,没有就是 libSystem。
这正是「省略即不作请求」这条规则在第三个平台上的同一次应用。
⭐ libSystem 不该有名字,而理由是实测出来的。
macOS 的三元组形状与另外两个平台不同 —— OS 段里带部署目标,且没有 env 段:
arm64-apple-macos14.0
^vendor ^os + 部署目标
clang 接受任意 env 后缀却不赋予任何含义,实测:
arm64-apple-macos14.0-gnu → 原样返回,未规范化
arm64-apple-macos14.0-musl → 原样返回,未规范化
原样保留而不像 Windows 那样重拼,说明 Darwin 那一支根本不看 env 段。
于是给 libSystem 起名会得到一个在编译器侧没有对应物的字符串。而它 本来也不需要名字:按「省略 = 不作请求,由图决定」,两种情况都被覆盖 ——
| 图里有 openkal | 结果 |
|---|---|
| 有 | c-abi musl (graph) |
| 没有 | libSystem(payload) |
gnu 是 LLVM 词表里真实存在、且映射到真实编译器行为的值(Windows 上
它决定对象 ABI);macOS 上没有任何对应物可映射。规范因此定:
macOS 的第三段只有一个合法值 musl,libSystem 由省略表达。
x86_64-linux-gnu 里的 gnu 严格说是项目名而非库名(glibc)。
保留它是为了兼容既有工程与 LLVM,而不是因为它准确。规范应当接受
glibc 作为别名并在文档中说明二者等价,新写的工程用哪个都对。
Windows 上 ABI 与 C 库在实践中一一对应,因此不需要第四段:
| os | libc | 派生的对象 ABI | 实测符号 |
|---|---|---|---|
| windows | gnu |
Itanium | _ZN2ns4Base1fEi |
| windows | musl |
Itanium | 同上 |
| windows | msvc |
MSVC | ?f@Base@ns@@UEAAHH@Z |
| linux / macos | 任意 | Itanium | — |
规范把这张表写成规范化的派生规则,而不是让每个消费者自己猜。
cfg(abi = "itanium") 因此可以被支持,且与 cfg(libc = "musl") 正交 ——
这解决了 §0.1 里 cfg(env=) 语义不定的缺陷。
ucrt 作为 CRT 而保持 Itanium ABI),一一对应就破了。届时的做法是
新增一个 libc 名(x86_64-windows-ucrt),而不是加第四段 ——
因为破的是「libc 只有三种」而不是「一个 libc 对一套 ABI」。
riscv64-none ← 规范形式
riscv64-none-elf ← 接受的别名(LLVM 拼法),规范化为上者
理由:第三段表示 C 库,而裸机没有 C 库。elf 是对象格式,
与 arch+os 一起已经决定,不需要单独命名。
target/riscv64-none-elf/ 变为
target/riscv64-none/。§4 给迁移方案。
| clang 的做法 | mcpp | 理由 |
|---|---|---|
Windows 上 gnu = 对象 ABI |
gnu = MinGW CRT |
第三段恒定表示 C 库(§1.1) |
无 windows-musl |
有 | 它真实存在(openkal-musl),且 clang 那条路是 ICE(§0.2) |
裸机带 -elf |
不带 | 那不是 C 库(§1.3) |
| 四段含 vendor | 三段 | vendor 不承重(§1) |
x86_64-unknown-linux-gnu |
x86_64-linux-gnu |
同上 |
学 clang 的地方:段的顺序、arch 与 os 的取值集合、musl/msvc
这些名字本身。偏离仅限于上表五处,每一处都有实测支撑。
规范的核心是一张双向映射表,而不是散落的 if。
mcpp → clang
x86_64-linux-gnu → x86_64-unknown-linux-gnu
x86_64-linux-musl → x86_64-unknown-linux-musl
x86_64-windows-gnu → x86_64-w64-windows-gnu
x86_64-windows-musl → x86_64-w64-windows-gnu ⭐ 与上一行相同
x86_64-windows-msvc → x86_64-pc-windows-msvc
aarch64-macos → arm64-apple-macos14.0
riscv64-none → riscv64-unknown-none-elf
⭐ windows-gnu 与 windows-musl 映到同一个 clang 三元组,这是设计而非
巧合。 交给 clang 的字符串回答「遵循哪套对象 ABI」,mcpp 的名字回答
「C 库是谁」。两个问题,两个答案;第二个 clang 答不了(§0.2)。
GCC 的目标编死在可执行文件名里,所以映射的是载荷与文件名:
mcpp → 载荷 / 编译器
x86_64-linux-gnu → xim:gcc / g++
x86_64-linux-musl → xim:musl-gcc / x86_64-linux-musl-g++
x86_64-windows-gnu → xim:mingw-cross-gcc / x86_64-w64-mingw32-g++
x86_64-windows-musl → (无 GCC 路径)
x86_64-windows-msvc → (MSVC,非 GCC)
riscv64-none → (clang 专属:见 §2.3)
x86_64-windows-musl 没有 GCC 路径,而这不是缺口:musl-on-Windows
是构建期体系的产物,其 C 库来自图而非载荷。规范应把「某目标在某工具链族下
无路径」记为一等状态,而不是让消费者从空字符串里推断。
x86_64-windows-msvc → cl.exe / clang-cl,由 SDK 提供 C 库。
实测:clang 在 Linux 上能为该目标编译(产出 ?f@@YAHH@Z 的 COFF),
但链接需要不可再分发的 MSVC SDK/CRT。规范应把这两件事分开记录 ——
「能否发码」与「能否链出成品」是不同的能力。
| 条目 | 内容 |
|---|---|
| 语法 | <arch>-<os>[-<libc>],段内字符集,大小写 |
| arch 取值 | x86_64 aarch64 riscv64 riscv32 … |
| os 取值 | linux windows macos none |
| libc 取值 | gnu(别名 glibc)musl msvc ucrt(预留);macOS 上仅 musl |
| 省略规则 | 省略 libc = 不作请求,由图或默认决定;身份仍完整 |
| 派生属性 | abi(itanium / msvc)、format(elf / pe / macho)、freestanding |
| 别名表 | riscv64-none-elf → riscv64-none,x86_64-w64-mingw32 → x86_64-windows-gnu |
| 映射表 | §2 的三张,双向声明,反向仅用于诊断 |
| cfg 谓词 | os arch libc abi format bare,不再有 env |
⭐ cfg(env = …) 应当被弃用而非直接删除:它今天在生态包里有使用者
(实测 openkal-windows 一处),删掉会让旧包在新引擎上加载失败 ——
这与 [[index-floor-must-degrade]] 记的教训同族。做法是接受它、
按 libc 求值、并在使用时告知新拼法。
| 变更 | 影响面 | 做法 |
|---|---|---|
新增 x86_64-windows-musl |
加法,无破坏 | 已实施(本轮) |
cfg(libc=) / cfg(abi=) |
加法 | 新增谓词,env 保留为别名 |
裸机去掉 -elf |
破坏:输出目录改名 | 别名 + 一个发布周期的过渡,-elf 拼法仍接受 |
| 去掉 vendor 段 | 无 —— mcpp 本来就没保存 | 仅文档化 |
riscv64-none 定为规范形式、riscv64-none-elf 定为别名,而输出目录
继续用别名,直到有别的理由动它。
- 没有断言
ucrt该怎么进表。 mingw-w64 支持ucrt作为 CRT 而保持 Itanium ABI,这会破坏 §1.2 的一一对应。⚠️ 需要先实测「mingw-w64 + ucrt」 在 mcpp 的载荷里是否可达,再决定是新增 libc 名还是引入第四段。 - 没有断言反向映射的完整规则。 §2.1 的映射不是双射,而诊断路径上 确实需要「从 clang 三元组说出 mcpp 目标」。规则应当是「多对一时报出全部 候选」还是「拒绝反推」,需要看诊断的实际用法再定。 (macOS 显式钉住 libSystem 的问题已移到 §6,作为一条明确记录在案的代价。)
本节记录明知而未解的东西。它们不是遗漏,而是判断「现在解的收益不明」 之后留下的账;每一条都给出重新打开它的触发条件,以免后来者需要重新 把上下文推导一遍。
规范定为「libSystem 由省略表达」(§1.1)。代价是说不出它:
aarch64-macos 图里有 openkal → musl 图里没有 → libSystem
aarch64-macos-musl ← 没有这个写法
aarch64-macos-??? ← 也没有这个写法
aarch64-macos-musl 显式请求 musl」,而词表里没有这一行。
实测(本 PR 构建出的二进制):
$ mcpp build --target aarch64-macos-musl
error: unknown target 'aarch64-macos-musl'
也就是说,macOS 这一轴上两个方向都说不出来:既钉不住 musl,也钉不住
libSystem,唯一的写法 aarch64-macos 把这件事整个交给图。Windows 那一轴
之所以能说出来,是因为本 PR 为它加了 x86_64-windows-musl 这一行 ——
macOS 缺的是同一件东西,而不是缺一条策略。
于是一个装了 openkal 依赖、却想在 macOS 上用 libSystem 的工程, 无法表达这个意图;反过来,一个想在 macOS 上明确钉住 musl 的工程,同样 无法表达。
x86_64-linux-gnu 恰恰就是「显式钉住 glibc」,
即使图里有 openkal-musl 也能说出来(今天的行为是报出分歧并以图为准 ——
见 check_request,那本身也是一条待议的策略)。
不解的理由:没有真实用例。给 libSystem 起的任何名字在 clang 侧都没有 对应物(实测 Darwin 不看 env 段),所以它只能是 mcpp 单方面的标记 —— 为一个没人提出过的需求引入一个只有一半意义的名字,代价比收益清楚。
重新打开它的触发条件,满足任一即可:
- 出现一个工程,依赖图里有 openkal 而在 macOS 上确实需要 libSystem
- openkal 支持了「同一构建里按目标选择 C 库」,使这个组合从边缘变成常规
- LLVM 在 Darwin 一支开始解释 env 段,使这个名字有了可映射的对象
届时的候选做法(不预先择一):aarch64-macos-system、
aarch64-macos-apple,或者引入一个与三元组正交的表达(如
[target.X] c-abi = "system")—— 最后一条可能更合适,因为它承认
「这不是三元组该回答的问题」。
ucrt 如何进表、反向映射的规则、裸机去 -elf 的时机 —— 见 §5。
它们与 6.1 的差别是:那些是尚未测,这一条是测过了并决定不做。
⭐ 一套目标词表的价值只有在第二个架构上才被检验。 x86_64 上「按 OS 分」
与「按架构分」给出相同答案,所以判据用错了轴也看不出来。本节把
aarch64-linux-musl 定为规范的验收目标,并记录逐层剥出的三处缺陷。
来源:#492 的使用者报告 —— 他需要同时出 x86_64 与 aarch64 两份静态二进制。
truncxfhf2.c:13:36: error: unknown type name 'xf_float'; did you mean 'tf_float'?
*xf*.c / *xc3.c 是 x87 80 位 long double 例程。排除它们的两处条件是
cfg(os = "macos") 与 cfg(os = "none"),而 aarch64-linux 落进
cfg(os = "linux") 那一支,没有排除。
long double 真是 x87 80 位,
-lgcc 拿掉后 ld.lld: error: undefined symbol: __mulxc3 会真的出现。
两个方向都会坏,错的只是轴。
修法:cfg(all(os = "linux", not(arch = "x86_64")))。
已提 mcpplibs/openkal-llvm-runtime#5。
x87 修好之后撞到下一处:
error: precompiled file 'std.pcm' was compiled with the target feature '-fmv'
but the current translation unit is not
error: current translation unit is compiled with the target feature
'+outline-atomics' but the precompiled file 'std.pcm' was not
⭐ 根因不是三元组不一致(两边都是 aarch64-unknown-linux-musl,
已从缓存里那条命令逐字核对),也不是缓存串目标(键含 target_triple,
两个 aarch64 条目正确地按工具链分开)。实测:
clang --target=aarch64-unknown-linux-musl → +outline-atomics
clang --target=aarch64-unknown-linux-musl --no-default-config → -fmv
--no-default-config 自己就改变目标特性。 std 模块带它编
(包的 std-module-flags 要求),普通 TU 不带。
clang++.cfg
无条件塞进宿主 C 库的头,不排除它,模块会在 <wchar.h> 上失败。
于是这是两个都成立的要求相撞:
| 要求 | 来自 | 为什么必需 |
|---|---|---|
std 模块要 --no-default-config |
包 | 否则宿主 C 库的头混进来 |
| std 模块与 TU 的目标特性必须一致 | clang | 否则 BMI 加载失败 |
--no-default-config 不改变特性,所以这对矛盾从未暴露。
aarch64 是第一个让它现形的目标 —— 这正是「第二个架构才检验抽象」的又一例。
修法:把 --rtlib=compiler-rt 补进 std-module-flags。
实测,补上之后两侧逐字相同:
TU (读 cfg) +fp-armv8 +neon +outline-atomics +v8a
std(旧:无 rtlib) -fmv +fp-armv8 +neon +v8a
std(新:+rtlib) +fp-armv8 +neon +outline-atomics +v8a
⭐ 声明在这个包里也是最诚实的位置:它 IS 那份 compiler-rt。 x86_64 上两侧本来都是空,所以该 flag 对它无影响 —— 这也再次说明为何 缺陷只在第二个架构上现形。
7.2 修好后撞到下一层:
ld.lld: error: undefined symbol: __aarch64_swp4_acq
+outline-atomics 需要 __aarch64_* 辅助函数,它们住在 compiler-rt 里 ——
而本包没产出。upstream 把 aarch64/lse.S 编 125 遍,每遍给不同的
-DL_<pat> -DSIZE=<n> -DMODEL=<m>,sources 的 glob 传不了 per-file 定义。
⭐ 解法是把宏从命令行移进文件,而不是复制 125 份实现:每个组合成为
一个只声明宏、再 include 共享正文的小文件,正文仍是 upstream 的、未改、
在原处。生成物放在 llvm-generated/outline-atomics/,与 llvm/ 分开 ——
与本包既有约定一致(std.cppm、__config_site 都在那里)。
#include "assembly.h" 在新目录下解析不到(汇编器把 HIDDEN(...)
读成指令),解法是让生成文件按相对自身的路径 include 那个头,不需要 flag;
以及 __aarch64_have_lse_atomics 未定义 —— 它在 cpu_model/aarch64.c,
而 glob builtins/*.c 不含子目录,那个文件从未被编过。
-mno-outline-atomics 加在 mcpp 引擎里,那是错的方向 ——
关掉一个本该支持的特性。有了辅助函数之后该 flag 已撤,引擎零改动。
验收判据(已达成):
mcpp build --target aarch64-linux-musl
→ ELF 64-bit LSB executable, ARM aarch64, statically linked
已定义的 __aarch64_* 符号 10,未定义 0
产物中 LSE 指令 8 处 ← 特性真的在工作
使用者撞到的原话是:
error: target 'aarch64-linux-gnu' is registered but not yet supported (planned)
⭐ 而他真正需要的是 aarch64-linux-musl(静态二进制),那一行是
verified。did_you_mean 今天只对拼错的
名字生效,不对「档位不够但同类目标可用」的情况生效。
使用者报告 _LIBCPP_HAS_RANDOM_DEVICE 0 让 5 个 TU 编不过,本地改成 1 即修好,
并问「静态 musl 二进制读 /dev/urandom 应当可行,generic 那份为何也关着」。
因果链逐层查证:
std::random_device
↓ libc++ 有五条后端:GETENTROPY / DEV_RANDOM / ARC4_RANDOM /
WIN32_RANDOM / FUCHSIA_CPRNG
↓ getentropy 是最合适的一条 —— 它不需要文件系统
musl 的 src/misc/getentropy.c → 调 getrandom()
↓
musl 的 src/linux/getrandom.c → syscall_cp(SYS_getrandom, …)
↓ 而 openkal-musl 的 port/ 里没有它的替代实现
openkal 规范里**没有随机源接口**(SURFACE.txt 全文无 random / entropy)
⭐ 根在最底下那一层。 另一条后端 DEV_RANDOM 走
open("/dev/urandom") + read,同样落在 openkal 的文件系统接口上,
而一个没有文件系统的后端(裸机)连这条也没有。
三层各自能做什么:
| 层 | 可行性 | 代价 |
|---|---|---|
| openkal 规范 | ⭐ 根本解 —— 新增 openkal.random 或等价接口 |
六个后端各实现一遍;按 6.1 条,不提供该接口的后端让它链接期缺席 |
| openkal-musl | 可以,但绕过规范 —— port 里实现 getrandom |
它仍要从某处取熵,绕不开平台层 |
| openkal-llvm-runtime | ❌ 只能开关那个宏 | 开了在链接期缺 getrandom,把编译错换成链接错 |
generic 那份也关着」的答案:generic 不等于「宿主 Linux」。
在这套体系里它同样跑在 openkal 之上,同样没有随机源 —— 名字容易误读,
而设置是对的。
openkal-musl/port/src/okm_syscall.c 已经把 69 个系统调用
转给了 openkal(SYS_read、SYS_openat、SYS_mmap、SYS_futex …)。
真正的约束不是「没有转的办法」,是规范里没有可转的目标。
为什么不能经 fs 打开 /dev/urandom。 这条路在设计上就被挡住:
kal_fs_open 只能相对 preopen 目录打开,而 openkal-linux 只给 2 个
preopen。⭐ 那正是能力模型要挡的东西 —— 一个程序不能凭一个绝对路径
够到平台上的任意对象。所以这不是缺口,是拒绝。
原则是「openkal 不加就能实现,就不加;但绝不能不走 openkal 接口层」。 逐条核对:
① 能不能不加就实现 —— 不能。
| 现有接口 | 能否供给熵 |
|---|---|
fs |
❌ kal_fs_open 只能相对 preopen 目录 —— 能力模型的拒绝,不是缺口 |
time |
❌ 时钟不是熵 |
env |
❌ 只有 arg / var 的读取,没有平台数据通路 |
| 其余六个 | ❌ 与熵无关 |
⭐ 而这个包已经诚实地回答过同一个问题。 okm_start.c 为 musl 的
AT_RANDOM 凑了 16 字节,注释写着:
openkal has no source of entropy and this layer does not invent one: the bytes below are derived from the clock and from the address of an object, which makes them unpredictable to a reader of the source and not to an adversary. … the README records that neither is a security property on this port.
那 16 字节给 allocator cookie 与 stack canary 够用 —— 它们本来就不承诺安全。
std::random_device 不够:那个类的存在意义就是不可预测。
② 能不能绕过接口层 —— 不能。 直接发 SYS_getrandom 违反原则,
而这正是本条最初的症状。
③ 它是不是通用内核能力 —— 宿主上是,裸机上不是。
| 后端 | 平台接口 | 有无 |
|---|---|---|
| linux | getrandom(2) |
✅ |
| macos | getentropy(2) / arc4random_buf |
✅ |
| windows | ProcessPrng / BCryptGenRandom |
✅ |
| uefi | EFI_RNG_PROTOCOL |
|
| opensbi | SBI 基础规范无 RNG 扩展 | ❌ 取决于板载外设 |
⭐ 「宿主普遍有、裸机不一定有」正是 openkal 能力模型为之设计的形状。
按 6.1 条,不提供该接口的后端让它作为链接期定义缺席 —— 与 openkal.fs
在 opensbi 上缺席完全同理。
⇒ 判定:该加。 三条都指向同一个答案,而第三条还说明它加进去之后 不需要任何新机制。
落地形状,两段:
长期(根本解) —— openkal 规范新增随机源接口。按 6.1 条, 不提供它的后端让相应符号链接期缺席,这正是规范已有的机制。
当下(可立即做) —— openkal-musl 加一个 feature。该包已有
[features](default = ["posix"]),机制现成:
[features]
host-entropy = { } # port 提供 getrandom,经宿主特定通路取熵host-entropy 而不是 random。
openkal-llvm-runtime 的 _LIBCPP_HAS_RANDOM_DEVICE 必须
跟随这个 feature,不能独立开关 —— 否则回到「宏开了、符号没有」,
把编译错换成链接错而已。
这条跨三个仓库(openkal 规范 / openkal-musl / openkal-llvm-runtime), 且必须先在规范里定接口。 它不属于目标词表这一 PR:词表讲的是「目标怎么 命名与映射」,这条讲的是「平台接口该不该多一个能力」,混在一起会让两件事 都说不清。
按 §7.5.1 的三条判定,该加。形状照 openkal.time —— 它是现有接口里最小的
一个,而随机源的问题结构与它同型:一件事,可能不被提供,提供时行为有差异。
/* openkal.random --- a source of unpredictable bytes.
*
* ⚠️ NOT A GENERATOR. This interface answers "give me bytes the platform
* considers unpredictable"; it does not define a PRNG, hold state, or promise
* a distribution. A program that wants a reproducible sequence seeds its own
* generator from these bytes and never comes back.
*/
#define KAL_RANDOM_PROP_BLOCKING ((kal_uintptr)1u << 0)
#define KAL_RANDOM_PROP_HARDWARE ((kal_uintptr)1u << 1)
extern const kal_uintptr kal_random_props;
/* Fills `len` bytes at `out`. Returns kal_ok, or an error.
*
* ⚠️ NO PARTIAL SUCCESS. Either every byte is filled or none is, and the
* distinction between "the source is momentarily empty" and "this environment
* has no source" is the difference between `kal_err_again` and the interface
* being absent at link time (clause 6.1).
*/
kal_status kal_random_fill(void* out, kal_uintptr len);两个能力字位,各自的理由:
| 位 | 含义 | 为什么程序需要知道 |
|---|---|---|
BLOCKING |
熵不足时可能阻塞 | 一个在早期启动路径上取随机数的程序会因此挂住;它需要能选择不那么做 |
HARDWARE |
直接来自硬件 RNG 而非内核池 | 影响的是信任模型,不是接口行为 |
KAL_RANDOM_PROP_AVAILABLE。 「有没有」由接口的在场与否回答
(6.1 条),不由能力字回答 —— 这正是 openkal-opensbi@0.1.3 那次撤回的教训:
给一个不提供的接口定义能力字,是回答第三个问题而跳过第二个。
六个后端的实现路径:
| 后端 | 实现 | 提供? |
|---|---|---|
| linux | getrandom(2) |
✅ |
| macos | getentropy(2) |
✅ |
| windows | ProcessPrng / BCryptGenRandom |
✅ |
| uefi | EFI_RNG_PROTOCOL |
|
| opensbi | 无 SBI 扩展 | ❌ 不提供 |
| 裸机通用 | 取决于板载 RNG | 由 BSP 决定 |
⭐ uefi 那一行是这个设计最有意思的地方:同一个后端在不同固件上
可能提供或不提供。按 6.1 条「实现提供一个接口是全有或全无」,
openkal-uefi 必须在构建期决定 —— 要么声明提供并在运行期
EFI_RNG_PROTOCOL 缺席时返回错误,要么整个不提供。
连带改动:
openkal-musl的port/加getrandom转发到kal_random_fill, 并把okm_start.c的AT_RANDOM从「时钟凑数」改为真随机 ——⚠️ 但要保留 fallback:该接口缺席时那 16 字节仍要有值,否则裸机上 连 stack canary 都起不来。openkal-llvm-runtime的两份__config_site把_LIBCPP_HAS_RANDOM_DEVICE改为跟随目标是否有该接口。- openkal 的一致性套件加一条:同一实现连续两次
kal_random_fill不得返回相同字节。⚠️ 这条会以极小概率误报,而那个概率是 2^-len*8 —— 在len=32时可以忽略,写清楚比省掉好。
clang++.cfg 把宿主 libc++ 与 glibc 的 -isystem 塞进来,与 openkal 的
musl libc++ 头混合。⭐ 与 7.2 是同一个 cfg 引起的两个症状,应一并考虑 ——
7.2 已由「把 --rtlib 补回 std 模块」解决,这一条尚未查。
- GMF 里
#include <标准头>的 TU 编不过 ——clang++.cfg把宿主 libc++ 与 glibc 的-isystem塞进来,与 openkal 的 musl libc++ 头混合。 ⭐ 与 7.2 是同一个 cfg 引起的两个症状,应一并考虑。
本节记录的是在推进九个仓库的过程中被判据抓住、而不是被设计预见到的问题。 每一条都附实测,以及它当时为什么没有被任何东西挡住。
这个生态里每个包发布的都是仓库在某个 tag 上的归档,所以对仓库的任何改动
都改变产物。一次发布因此需要 mcpp.toml 的 version 前进,而没有任何地方在
检查它有没有前进 —— 每个包的 CI 校验的是眼前这棵树,对「哪些数字已经被
占用」没有意见。
实测(2026-08-25,跟随 openkal 0.7.0 走完生态):八个仓库里有六个待合入的
分支,其版本号等于 main 的版本号,也就是索引里已有的版本号;其中两个还新增
了一整个接口实现。六个全都是绿的。
git tag 0.5.3 发现 tag 已存在便成功;从那个
tag 取下来的归档是旧内容;两条镜像腿于是彼此一致、也与源归档一致 ——
校验完美通过,而什么都没有发布。唯一残留的痕迹是索引里第二个 ["0.5.3"],
而 Lua 接受重复键、保留最后一个、一声不吭。
修法落在索引,因为那是「什么已经发布」的唯一记录,一处覆盖生态里所有包:
tests/check_duplicate_versions.lua,文本扫描而非加载描述符 —— 加载正是看不见
它的那种做法,因为表建起来的时候重复已经合并掉了。128 个描述符全绿;把 openkal
的 0.7.0 条目插两遍,它按文件、行号、平台段点名,而同一份文件 loadfile 成功。
一张图里出现同一个包的两个主版本时,解析器把其中一份改名暂存到
target/.mangled/。暂存按设计只搬源码:经 [build].include_dirs 找到的头
靠绝对化后的路径仍指回原处。
写在源码旁边的私有头是另一回事,而它没有被处理。#include "detail.h" 是
相对持有该指令的文件所在目录解析的,搬走源码就搬走了搜索起点;并且没有任何
include_dirs 条目参与其中,因为一个把头放在源码旁边的包从来不需要声明
路径。
已修(mcpp #502),并用两个二进制而不是一个二进制加一个假设来证明:
mcpp-unfixed 33_multi_version_mangling.sh → fatal error: libB_detail.h
mcpp-fixed 33_multi_version_mangling.sh → ok,与源码一同暂存
tests/e2e/284 里,验证「mcpp 自己的名字进去、编译器认识的拼法出来」这条映射的
分支写着 *) : ;; —— 匹配就打印一行 ok,不匹配也通过。而这条映射正是本 PR
新增那一行的全部意义。已改为可失败,并在旁边补上对 pin 的断言。
⭐ 同型排查:e2e 全库另有 5 处 catch-all 分支,逐个读过,全部是合法的平台分派或 带理由的 SKIP,不是伪装成断言的空操作。
openkal-musl 的 CI 把规范与实现的工作树放到包旁边,取的是与当前分支同名的
分支,没有同名分支时回落到默认分支。这是对的 —— 这里多数分支本来就没有对应物 ——
但一次跨两个仓库的改动,于是会对着「哪一半恰好在 main 上」构建。
实测:本包的分支叫 feat/getrandom-through-openkal,规范的叫
feat/openkal-random,于是一个要求 0.7.0 的清单拿到了 openkal 0.6.0,五个 job
报的是:
port/src/okm_syscall.c:27:10: fatal error: 'openkal/random.h' file not found
缺头文件读起来像是本包写错了。 错的是两半不同步,而这正是新加的门要说的话。 硬失败也是唯一安全的答案:如果规范那半的改动是向后兼容的,同样的错配会 通过 —— 而被测的是没有人在审的那份规范。
判据用的是清单本来就写着的版本要求,只是被「改写成 path」这一步绕过去了:
tree 0.6.0 → exit 1,"they are not in step"
tree 0.7.0 → exit 0
tree 0.7.4 → exit 0 补丁位可以前进
tree 0.8.0 → exit 1 1.0 以下,minor 前进即为破坏性
见 §6.1 的更正。aarch64-macos-musl 曾被本文列为「显式请求 musl」的写法,
而词表里没有这一行,实测 error: unknown target。随后把 docs/16(中英两份)
与三份分析文档里出现的全部 23 个目标名逐一对着词表核过:四个是填充规则下
合法的短拼法,四个出现在明说「不采用」的段落里,只有这一个被当作真的写着。
接口表少了 openkal.random(本次新增)与 openkal.exec(已 optional 很久且
有头文件)。SURFACE.txt 和 README 的表是同一份清单的四种陈述,而 CI 只比对前三种 ——
表面检查器读的是 SURFACE.txt 和头文件,README 对它来说是散文。这就是一行
能在若干次发布里一直缺着的原因。
openkal-llvm-runtime 里 !llvm/libcxx/src/random.cpp 这条排除被删掉,对
hosted 是对的 —— 它同时把 _LIBCPP_HAS_RANDOM_DEVICE 打开了。但 freestanding
那份配置仍是 0,而同一次删除也波及了它:
llvm/libcxx/src/random.cpp:68:1: error: use of undeclared identifier 'random_device'
开关关掉时 <random> 不声明这个类,而 src/random.cpp 无条件定义它的成员 ——
这个文件自己不带守卫,不像旁边的 filesystem 源码。
判据取的是构建图而不是「编过了」:libcxx/src/random.cpp 在 freestanding 的
build.ninja 里出现 0 次(与已知被排除的 filesystem 一致),而应当在的
random_shuffle.cpp 出现 2 次。再把排除注释掉重建 —— 复现同一条错误。
openkal.random 是可选接口,裸机后端不提供它。而 openkal-musl 的系统调用
分发器无条件引用 kal_random_fill,且这个分发器被链接进每一个程序:
ld.lld: error: undefined symbol: kal_random_fill
>>> referenced by okm_syscall.c:409
于是一条可选接口的缺席,变成了所有裸机程序的链接失败 —— 哪怕它一个随机 字节都不要。这是 6.1 条在另一个方向上的后果:该条把「不提供」表达为「没有定义」, 而任何无条件引用都会把这句话翻译成「必须提供」。
⭐ 可选接口的转发者必须弱引用,缺席时返回该层自己的「没有这个东西」的答案。
这里是 ENOSYS —— Linux 系统调用 ABI 对未实现调用的定义答案,而 musl 自己的
getrandom 正是照着它写的。
okm_phdr.c 就在用
__attribute__((weak)),而 okm_syscall.c 开头第 11 行写着「能被告知没有的
调用返回 -ENOSYS」。
这条缺陷发布出去了,而 openkal-musl 自己的 CI 一格都没看见它 —— 是下游
openkal-llvm-runtime 的裸机程序把它顶出来的。补的检查落在符号类别上,
不需要裸机工具链,也比一次链接更直接地陈述这个不变量;并且自带控制项:
有 weak w kal_random_fill U kal_time_sleep → 绿
去掉 weak U kal_random_fill U kal_time_sleep → 红
只断言「random_fill 是弱的」会在符号消失或对象没被构建时同样通过,所以旁边那个 必需接口必须仍是未定义强引用。