Skip to content

Commit f3e4ec9

Browse files
committed
docs: deep review of #455-#459 and the bare-metal ecosystem
Covers what the five PRs did, the four releases they produced, and the ecosystem work alongside them (xim-pkgindex #651/#652/#653, mcpp-index #219/#220, two new mcpplibs repos), assessed on architecture, compatibility, simplicity, stability and cross-platform. Every claim carries its source — a PR number, a file, or the command that measured it — because the point of the document is to be checkable rather than summarised. Three of its assertions were re-verified against the tree while writing it. The uncomfortable half is deliberate: * five defects in this round were self-inflicted, two of them found only AFTER a release; * three test criteria were themselves wrong — green tests that were not testing the thing they named; * the largest architectural error (packages declaring the target's C library) was found by review, not by me, and I had written 'this cannot be done' about the std subset while my own research document had measured that it could.
1 parent 0ad5b52 commit f3e4ec9

1 file changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# PR #455#459 深度 review:裸机 freestanding 从「能编」到「能用」
2+
3+
**范围**:`mcpp-community/mcpp` #455 · #456 · #457 · #458 · #459,以及同期的
4+
`openxlings/xim-pkgindex` #651/#652/#653(+ 三条版本 bump)、`mcpplibs/mcpp-index`
5+
#219/#220、两个新建仓库 `mcpplibs/riscv-virt-rt``mcpplibs/std-freestanding`
6+
7+
**时间**:2026-08-19 一日之内(#455 合入 → #459 合入),发布 `2026.8.19.1``2026.8.19.4`
8+
9+
⚠️ **本文的定位是 review 而不是总结**:凡是「做了什么」都给出可复核的出处(PR 号、
10+
文件、命令),凡是「做错了什么」都写明**发现方式****当时为什么没看见**
11+
一日四个补丁版本本身就是一个信号,第 6 节专门分析它。
12+
13+
---
14+
15+
## 0. 一句话结论
16+
17+
引擎侧的形状是对的,而且**收敛得比计划更小**;真正的成本几乎全在
18+
**「我把属于 target 的东西写成了包的依赖」** 这一类边界判断上 ——
19+
五个已修缺陷里有三个是同一个根因的不同表现。
20+
21+
判据(用**已发布**二进制、从零验证):
22+
23+
```bash
24+
mcpp new blinky --template riscv-virt-rt
25+
cd blinky && mcpp run # qemu 里打印;manifest 里没有 [target.*] 段
26+
```
27+
28+
```toml
29+
[package] name = "blinky" / version = "0.1.0"
30+
[build] target = "riscv64-none-elf"
31+
[dependencies] riscv-virt-rt = "0.3.0"
32+
```
33+
34+
---
35+
36+
## 1. 五个 PR 各做了什么
37+
38+
| PR | 规模 | 内容 | 版本 |
39+
|---|---|---|---|
40+
| **#455** | +6355/−18,34 文件 | 裸机 target 成立:`riscv64/32-none-elf` 进目标表、新建 `src/freestanding/`(`target`/`linkline`/`runner`)、`mcpp:link-script=` 指令(protocol 3)、`mcpp::xpkg_dir` | 2026.8.19.1 |
41+
| **#456** | +2/−2 | 发布 ||
42+
| **#457** | +1388/−49,24 文件 | **runner 归 BSP**(`mcpp:runner=`,新 `Slot::Runner` + `Scope::RunGlobal`,protocol 4)、裸机 `mcpp test`、产物集(`.bin`/`.map`/size 摘要) | 2026.8.19.2 |
43+
| **#458** | +109/−3,5 文件 |`mcpp build` 之后 `mcpp run` 在宿主上直接 exec 裸机 ELF | 2026.8.19.3 |
44+
| **#459** | +753/−45,21 文件 | 异常/RTTI 整图关闭、**目标自带 sysroot 轴**`toolchain_dir`/`sysroot_dir`、依赖缓存键补 target flags、std 子集打通 | 2026.8.19.4 |
45+
46+
累计(从 #455 之前的 `9c5cc8d` 算起,含同期其它工作):**87 文件 / +10330 / −161**
47+
其中 `src/freestanding/` 三个模块共 **432 行**,裸机 e2e 四个脚本共 **983 行**,
48+
`test_freestanding.cpp` **25 个单测**
49+
50+
**指令表 13 → 15 行,protocol 2 → 4。** 两条新指令(`link-script``runner`)各自
51+
只占表里一行 —— 这是 #455/#457 最值得记的一点,见 §2.1。
52+
53+
### 生态侧
54+
55+
| 仓库 | 内容 |
56+
|---|---|
57+
| `openxlings/xim-pkgindex` | #651 `qemu-riscv 9.2.4-1`(五个宿主目标)· #653 `picolibc-riscv 1.8.12`(自建,`rv64gc/lp64d` + `rv32imac/ilp32`#652 修镜像工具两个缺陷 · #654/#655/#657 版本 bump |
58+
| `mcpplibs/mcpp-index` | #219 收录 `riscv-virt-rt`(已合)· #220 `std-freestanding 0.2.0` + `riscv-virt-rt 0.3.0`(待 2026.8.19.4 生效) |
59+
| `mcpplibs/riscv-virt-rt` | 板级支持包,0.1.0 → **0.3.0**;含 `templates/blinky`(`mcpp new --template`)、examples、仓库 CI(rv64+rv32 真跑 qemu) |
60+
| `mcpplibs/std-freestanding` | freestanding std 子集,0.1.0 → **0.2.0**;`tools/regenerate.sh` 实测生成 |
61+
62+
两个包 GitHub + GitCode 双镜像,tarball **逐字节校验一致**
63+
64+
---
65+
66+
## 2. 架构评估
67+
68+
### 2.1 ⭐ 最好的一条:能力增长没有换来概念增长
69+
70+
裸机需要三样东西是引擎原本表达不了的:**链接脚本****执行方式****目标 C 库位置**
71+
三样落地为:
72+
73+
- `link-script` —— 指令表**一行**(`Scope::LinkGlobal`,`Transform::LinkerScript`);
74+
- `runner` —— 指令表**一行** + 一个新 `Slot` + 一个新 `Scope`;
75+
- 目标 sysroot —— `TargetInfo` **一个字段**,与 `pin` 并列。
76+
77+
⚠️ **计划里原本有更大的方案,是实测把它们砍掉的**:
78+
79+
| 计划写的 | 实测 | 结果 |
80+
|---|---|---|
81+
| W8「带序 startup-objects 具名槽」 | `-lcrt0-semihost`**归档**里拉得进来,顺序由链接脚本 section 序决定 | ⛔ 整个槽位机制不需要,真正缺的只有 `-T` |
82+
| §4「裸机 `mcpp test` 要 batch 模式 + 结构化 stdout 协议」 | qemu 冷启动 **12ms**(不是计划写的 0.4s);semihosting **把固件 `main` 返回值原样传给 qemu 退出码** | ⛔ 六个设计决定里四个不必做,R3 缩成「走 `mcpp run` 用的同一个 runner」 |
83+
| W12 `requires-hard` 能力硬门 | 同一个 token 要同时表达「平台本来没有」与「runner 配错了」 | ⛔ 实现后用了一次,macOS e2e **一小时内证伪**,整个机制撤销 |
84+
85+
**这三条是本轮质量的主要来源**:计划里凡是带具体数字或「A 不成立所以要 B」的句子,
86+
数字与断言本身就是必须先测的探针。
87+
88+
### 2.2 ⭐ 作用域的不对称是刻意的,而且被两侧钉住
89+
90+
`directives.cppm``link-search`/`link-lib`/`link-script`**LinkGlobal**(到达消费者),
91+
`include-dir`/`cflag`/`cfg`**PackagePrivate**(不到达)。这条供应链纪律让
92+
**引擎完全不需要 sysroot 概念**就能让 BSP 工作:目标头由包私有 include、对外只 export 一个模块。
93+
94+
e2e/131 从**两侧**钉它:目标 C 头**必须**到达消费者,板级包**自己的****必须不**到达。
95+
96+
### 2.3 ⚠️ 最大的架构错误:把 target 的属性写成了包的依赖
97+
98+
`riscv-virt-rt``std-freestanding` 都曾声明
99+
`[xlings] deps = ["xim:picolibc-riscv@1.8.12"]`,后者还声明了 `xim:llvm`
100+
**这是用户 review 时指出的**,三条都对:不该绑 libc、不该绑架构、不该绑编译器实现。
101+
102+
真因是**引擎的结构性缺口**,不是包写得随意:
103+
104+
| | 编译器 | 目标 libc |
105+
|---|---|---|
106+
| hosted(`x86_64-linux-musl`) | 目标表 `pin` **自动** | musl 在 gcc 载荷里 / glibc 走 `PayloadPaths` —— **自动**,从没人写过 `xim:glibc` |
107+
| freestanding | 目标表 `pin = llvm@22.1.8` **自动** | **一条轴都没有** ⇒ 外溢到每个包 |
108+
109+
#459`TargetInfo``sysroot` 轴,复用现有 `[xlings] deps` 物化通道安装,
110+
引擎发 `-isystem <sysroot>/include/<档位>``-L <sysroot>/lib/<档位>`
111+
再补两个「问引擎」的接口 `mcpp::toolchain_dir()` / `mcpp::sysroot_dir()`
112+
113+
**结果**:`std-freestanding` 变成**零依赖**,`riscv-virt-rt` 只剩模拟器(那确实是板级事实)。
114+
分界线也随之清楚了:**位置是目标的事实,选择是板级的事实。**
115+
116+
⚠️ **这个错误能存在两个版本,是因为它在装好的机器上完全不可见** —— 见 §5.2。
117+
118+
### 2.4 单一读取点
119+
120+
`choose_runner(ctx)` 是「产物怎么执行」的**唯一**读取点,`mcpp run``mcpp test` 共用。
121+
这是本仓库反复付过学费的形状(#233/#240/#242/#344:同一决策两处推导)。
122+
覆盖语义也留了:`[target.X].runner` 覆盖 BSP 供的,调试时换 `-bios` 是正当需求。
123+
124+
---
125+
126+
## 3. 兼容性评估
127+
128+
### 3.1 ⭐ 已发布数据不得让已发布程序失效
129+
130+
索引 `min_mcpp` **没有抬**(memory:`index-floor-must-degrade`)。
131+
新包进索引不会让老客户端拿不到任何包。`riscv-virt-rt` 0.1.0/0.2.0 **留在索引里**,
132+
因为「已经发布过的版本就是可能有人 pin 过的版本」。
133+
134+
### 3.2 ⚠️ 类型化 API 没有语言内的特性探测(实测)
135+
136+
```cpp
137+
if constexpr (requires { mcpp::runner("x"); }) // 名字不存在 ⇒ 硬错误,不是 false
138+
```
139+
140+
`requires` 表达式作用在**不存在的限定名**上是 ill-formed。⇒ 包**无法**优雅降级;
141+
而 wire 协议那条点名 `mcpp self update` 的诊断也够不着(它要求程序**先编译得过**)。
142+
143+
⇒ 补在引擎侧:`build.mcpp` 编译失败且错误含「不是 `mcpp` 的成员」时追加升级提示,
144+
三个前端三种拼写都认。**对以后每一次类型化 API 新增都生效**,不只是 `runner`。
145+
146+
### 3.3 ⚠️ 升级即坏的缓存缺陷(#459 修)
147+
148+
依赖缓存键有 `targetTriple`,**没有 triple 隐含的那组 flag**。而**哪些 flag 由 triple 隐含
149+
是 mcpp 的决定**,会随版本变、triple 字符串不变。
150+
151+
⇒ 加 `-fno-exceptions` 那一刻,所有已建过的裸机工程**复用升级前的 BMI**,硬失败,
152+
而错误只点名一个 `.pcm` 文件:
153+
154+
```
155+
error: exception handling was enabled in precompiled file
156+
'mcpplibs.riscv_virt_rt.pcm' but is currently disabled
157+
```
158+
159+
已加 `targetImpliedFlags` 轴(hosted 为空,不动任何现有键),单测钉「加一个 flag 键必须变」。
160+
161+
### 3.4 ⚠️ 一条指向不存在的包的诊断(自造,已修)
162+
163+
第二阶段的 `import std` 诊断末尾给了可直接粘贴的
164+
`mcpplibs.std.freestanding = "0.1"` —— **这个包当时没发布**。用户粘完下一条命令报
165+
`package not found`。⇒ **诊断里的每条建议都是承诺**;现在包发布了,那一行才放回去。
166+
167+
### 3.5 破坏性变更盘点
168+
169+
| 变更 | 影响面 | 处置 |
170+
|---|---|---|
171+
| `mcpp run` 位置参数改名 `target` → `bin` | **零**(只出现在 `--help`,`cmd_run` 按下标读) | — |
172+
| `mcpp run --target-triple` | 保留为别名(2026.8.19.1 已发布的拼写),e2e/130 两个拼写都钉 | — |
173+
| freestanding 加 `-fno-exceptions -fno-rtti` | 裸机工程全体 | 由 §3.3 的缓存键保证重编;**异常在裸机上本来就不可用**(无 unwinder) |
174+
| 目标自带 sysroot | 老包的 `[xlings] deps` 仍可留(去重),**不破坏** | 新包不必写 |
175+
176+
---
177+
178+
## 4. 简洁与优雅
179+
180+
**好的**:
181+
182+
- `src/freestanding/` 三个模块 **432 行**承载全部裸机知识,且 `linkline` 是**纯字符串构造**
183+
(不依赖 `CompileFlags`,避免模块环)。
184+
- ISA 表是**数据**:`riscv32` 支持靠表里加一行,引擎与 BSP 零改动 —— 这条被 N3 判据实测过。
185+
- 模板**随包走**:`cmd_new` 的内建注册表刻意冻结在 `bin`,其余是包模板;
186+
`mcpp new` 把自依赖按**解析到的版本**写进 manifest ⇒ **模板不可能与库脱节**。
187+
⚠️ 计划里原本要给 mcpp 加内建 `--template baremetal-riscv`,**是错的形状**。
188+
- std 子集**不手写导出表**:libc++ 自带 110 个 `std/<header>.inc`,子集是机械挑选,
189+
生成物 217 行全是 `#include`;`tools/regenerate.sh` 每次运行都打印宿主对照组。
190+
191+
**不够好的**:
192+
193+
- ⚠️ `src/build/prepare.cppm` 与 `execute.cppm` 各 +136/+137 行。这两个文件已经很大,
194+
本轮又往里加了 sysroot 解析、runner 选择、size 报告、快路径前置条件。
195+
**它们正在变成「什么都知道的地方」**,下一轮应当先拆再加。
196+
- `build.mcpp` 里合成 `__config_site` 是 40 行字符串拼接。可用但不优雅;
197+
若第二个包也要合成配置,应当抽成 `mcpp::` 的一个接口。
198+
199+
---
200+
201+
## 5. 稳定性评估
202+
203+
### 5.1 已修缺陷清单(全部为本轮自造)
204+
205+
| # | 缺陷 | 发现方式 | ⚠️ 当时为什么没看见 |
206+
|---|---|---|---|
207+
| 1 | `mcpp run <binary>` 被 `--target` 吃掉 | **CI(e2e/73)** | 位置参数上**原本就有注释写着这个碰撞**,我为了「一致性」覆盖了它 |
208+
| 2 | `mcpp build` 后 `mcpp run` 在宿主 exec 裸机 ELF | **发布后**用发布二进制走新用户流程 | 130/131/132 三个 e2e **都恰好先 `run` 后 `build`**,顺序本身是被测对象 |
209+
| 3 | 诊断指向不存在的包 | 自查 | 写诊断时没验证那一行今天能跑通 |
210+
| 4 | 包声明了 target 的 libc/编译器 | **用户 review** | 在已装好的机器上,这条边在与不在**完全一样** |
211+
| 5 | 缓存键缺 target 隐含 flags | 实施 #459 时的升级路径自测 | 只有跨版本才暴露 |
212+
213+
### 5.2 ⚠️ 三条「判据本身是错的」
214+
215+
这是本轮最值得记的一类问题 —— 测试是绿的,但它测的不是那件事。
216+
217+
1. **第一版 build-then-run 回归测试是假绿**:加进 e2e/131 的既有工程后,
218+
**关掉修复它照样过**。给 `try_fast_run` 的 16 个 `return nullopt` 各打编号探针后定位到:
219+
`mcpp.toml` 比 `build.ninja` 新,**快路径本来就没被走到**。
220+
真因:**重建不会重写内容未变的 `build.ninja`** ⇒ 在原地编辑过 manifest 的目录里,
221+
manifest 永远最新,快路径**永久关闭**。⇒ 这类测试必须**新建干净工程**,并先确认它**变红**。
222+
2. **e2e/131 的私有性断言判据失效**:它用「`#include <stdio.h>` 编不过」证明依赖的
223+
`include-dir` 不到达消费者。libc 归目标之后 `<stdio.h>` **本来就该编得过**(和宿主一样)。
224+
**原来那条测的其实是「libc 从哪来」,不是「作用域对不对」。**
225+
3. **「拿走再装回来」**:xim 安装期依赖边在**已装好的机器**上不可验证。
226+
判据必须是把 store 里的包改名藏起来 → `mcpp add` + `mcpp build` 把它装回来。
227+
228+
### 5.3 ⚠️ `.map` 曾是「写了但没人跟踪」
229+
230+
`-Wl,-Map=` 是链接命令上的 flag,不是独立边,所以很容易只写不声明 ——
231+
删掉 `firmware.map`,ninja 认为 ELF 是新的、什么都不做,map 就永远不回来。
232+
已声明为链接边的 implicit output,e2e/132 用「删掉必须回来」钉住(**这是唯一能在修复前失败的形式**)。
233+
234+
### 5.4 测试面
235+
236+
- 单测:`test_freestanding.cpp` 25 个 + `test_build_directives` 33 个 + cache_key 19 个。
237+
- e2e:130(引擎链)/131(生态链)/132(test + 产物)/133(std 子集),共 983 行。
238+
- CI:新增 `baremetal` job,**两个 home 都装 qemu**(shim 按拥有它的 home 派发),
239+
并且**断言每条 PASS 行真的出现** —— `run_all.sh` 跳过时退出码是 0。
240+
- 包侧 CI:`riscv-virt-rt` 与 `std-freestanding` 各自仓库 CI 真跑 qemu,rv64+rv32 双档。
241+
242+
---
243+
244+
## 6. ⚠️ 一日四个补丁版本:这个节奏说明什么
245+
246+
`2026.8.19.1` → `.2` → `.3` → `.4`,其中 `.3` 与 `.4` 都是在**发布之后**发现问题才发的。
247+
248+
**不是坏事的部分**:`.3` 的缺陷是靠「用发布二进制从零走一遍新用户流程」发现的 ——
249+
这一步如果省掉,它会留在生态里直到有用户撞上。**这一步的价值被证明了。**
250+
251+
**是问题的部分**:
252+
253+
- `.2` 发布时,`mcpp build && mcpp run` 这条**新用户最可能敲的两条命令**没有被任何测试覆盖。
254+
四个裸机 e2e 都先 `run`,是巧合而非设计。
255+
- `.4` 的架构问题(§2.3)是**用户 review 发现的**,不是我自查发现的。我在 `.3` 的收尾报告里
256+
甚至把 std 子集写成「要从头实现、不该在收尾时半做」——**而调研文档 X1–X10 早就把这条路测到了底**。
257+
⇒ **写「这条做不了」之前必须回查自己的调研文档。**
258+
259+
**改进项**(下一轮的硬约束):
260+
261+
1. 发布前必须跑一遍**命令序列矩阵**(`build→run`、`run→build`、`build→test`),而不是单条命令。
262+
2. 任何「某某做不了 / 代价很大」的结论,若同一课题有调研文档,**必须引用文档中的具体测量**才能下。
263+
3. 回归测试必须做 **revert-A 探针**(关掉修复看它是否变红),否则不算写完。
264+
265+
---
266+
267+
## 7. 跨平台评估
268+
269+
⭐ **裸机 target 反而是最省跨平台成本的一类**:clang/lld 是 cross-compiler by construction,
270+
**任何能装 llvm 载荷的宿主都能产出 RISC-V 镜像**,不需要 per-host 交叉载荷。
271+
`host_can_serve` 对 freestanding 直接返回 `true`,这条在目标表注释里写明了。
272+
273+
| 轴 | 状态 |
274+
|---|---|
275+
| 宿主 | 引擎侧 21 个 CI job 全绿(linux/macOS/Windows/aarch64/hermetic/mingw-cross) |
276+
| 模拟器 | `xim:qemu-riscv` 覆盖 **五个宿主目标**(linux x64/arm64 · darwin x64/arm64 · win32 x64) |
277+
| 目标 sysroot | `picolibc-riscv` 与宿主无关(目标侧产物),三平台同一份 |
278+
| ⚠️ 缺口 | `qemu-riscv` **win32-arm64 上游无资产** ⇒ 该宿主装不上,行为正确但会失败 |
279+
| ⚠️ 缺口 | 两个包的仓库 CI **只有 ubuntu-24.04**;macOS/Windows 上的裸机链**没有持续验证** |
280+
281+
⚠️ **mcpp-index 的 `tests/examples/` workspace 成员在三个平台上无条件跑,没有能力门**
282+
⇒ 需要模拟器/目标 sysroot 的包**不能加成员**,验证只能放包自己的仓库 CI。
283+
这是一个**已知的覆盖缺口**,不是遗漏。
284+
285+
---
286+
287+
## 8. 仍然开着的
288+
289+
| 项 | 性质 | 备注 |
290+
|---|---|---|
291+
| **T3 档**(`std::format`、标量 `std::sort`、完整 `std::string`) | 真实边界 | libc++ 把这些实体放在编译版库里(标量 `__sort` 是 `extern template`,**无宏可关**)⇒ 需为目标编 `libc++.a`,是新载荷 |
292+
| `mcpp-index` #220 | 待发布生效 | 依赖 2026.8.19.4 |
293+
| 目标表**索引化** | 已定为阶段二 | 把 `pin` + `sysroot` 搬到索引,含老客户端降级路径;阶段一不会让它更难做 |
294+
| 第二块板 / ARM Cortex-M | 未开始 | rv32 已作为「ISA 表是数据」的证据 |
295+
| `prepare.cppm` / `execute.cppm` 体量 | 技术债 | 见 §4 |
296+
| 包侧 CI 单平台 | 覆盖缺口 | 见 §7 |
297+
298+
---
299+
300+
## 9. 分项评分
301+
302+
| 维度 | 评价 | 依据 |
303+
|---|---|---|
304+
| **架构** | 好 | 能力增长未换来概念增长(2 条指令 + 1 个字段);三个计划中的大机制被实测砍掉;单一读取点 |
305+
| **兼容性** | 好,但有一次真实风险 | 索引底线未抬、别名保留、破坏面为零;⚠️ 但 `-fno-exceptions` 差点造成升级即坏,靠缓存键轴补上 |
306+
| **简洁优雅** | 好 | 432 行承载全部裸机知识;ISA 表是数据;模板随包走;子集不手写导出表 |
307+
| **稳定性** | ⚠️ 中 | 五个自造缺陷,两个是**发布后**才发现;三条判据本身是错的 |
308+
| **跨平台** | 好,有已知缺口 | 引擎侧 21 job 全绿、模拟器五宿主;⚠️ 包侧 CI 单平台 |
309+
| **一致性** | 好 | `--target` 在所有子命令上拼写一致(靠位置参数改名换来,而非放弃一致性) |
310+
| **无感升级** | ⚠️ 中 | 缓存键轴与诊断升级提示都是**补出来的**,不是设计时就有的 |
311+
312+
---
313+
314+
## 10. 三条要带进下一轮的
315+
316+
1. ⭐ **计划里的数字就是探针。** 本轮被实测推翻的计划主张:qemu 冷启动(0.4s→12ms)、
317+
「裸机没有退出码」(semihosting 有)、builtins 缺口触发者(不是 64 位除法,是 128 位移位)、
318+
「libc++ 头一个都用不了」(103/110)。**四条全是我没测就写下的。**
319+
2. ⭐ **在「已经配置好的机器」上,很多边验不出来。** 目标 libc 的依赖边、xim 安装期依赖边、
320+
快路径前置条件 —— 三者的共同点是**存在与不存在长得一模一样**。
321+
判据必须是**拿走**(stash 后重装)或**新建**(干净工程),不能是「在我这儿是好的」。
322+
3. ⚠️ **顺序本身是被测对象。** `mcpp run` 对、`mcpp build && mcpp run` 错,
323+
而四个裸机 e2e 恰好都先 `run`。**命令序列矩阵要进发布前检查表。**

0 commit comments

Comments
 (0)