Skip to content

Commit eee3e8e

Browse files
committed
docs: user-facing bare-metal scenarios, and the package-naming/ownership answers
Adds a scenario document — what a user types, what they see, and what they no longer have to write — with the commands and outputs taken from a real run of the released binary rather than sketched. Folds two review questions into the analysis: * `picolibc-riscv` / `qemu-riscv` carrying the target in the name follows xim's existing rule, which is visible across the index: aarch64-linux-musl-gcc, riscv64-linux-musl-gcc, mingw-cross-gcc, musl-gcc. The NAME carries the target; the `archs` axis carries the host, which is why `llvm` has no target in its name at all. * picolibc belongs on the xim side, resolved at build time from the target's row. It is not importable, it is chosen by the target rather than by a dependency graph, and every other C library in the ecosystem — glibc, musl, musl-cross-make — is already there; mcpp-index carries zero libc packages. Moving it would put the libc back into the package graph, undoing #459. Both answers came with a gap worth recording: `[target.X]` has toolchain, linkage, runner and cxx_runtime but no `sysroot`, so a project cannot swap picolibc for newlib today.
1 parent f3e4ec9 commit eee3e8e

2 files changed

Lines changed: 372 additions & 1 deletion

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# 裸机:用户面能感受到的变化(场景 + 伪代码)
2+
3+
配套 [PR #455#459 review](2026-08-20-pr455-459-freestanding-review.md)
4+
本文只讲**用户敲什么、看到什么、不用写什么**;机制在 review 里。
5+
6+
以下命令与输出都是**实测**的(mcpp 2026.8.19.4 + `riscv-virt-rt 0.3.0`),
7+
不是设想的接口。
8+
9+
---
10+
11+
## 场景 0:先看「不用写什么」
12+
13+
这是本轮最大的用户面变化。裸机工程的**整个** manifest:
14+
15+
```toml
16+
[package]
17+
name = "blinky"
18+
version = "0.1.0"
19+
20+
[build]
21+
target = "riscv64-none-elf"
22+
23+
[dependencies]
24+
riscv-virt-rt = "0.3.0"
25+
```
26+
27+
⭐ 里面**没有**:链接脚本路径 · 加载地址 · `-nostdlib` · `-march`/`-mabi`/`-mcmodel` ·
28+
crt0 · libc 名字 · 模拟器命令行 · `[target.*]` 段 —— **一个都没有**
29+
30+
对照:同样的工程在裸机 C/C++ 的常规做法里,通常需要一份 `link.ld`、一份 `start.S`
31+
一段 Makefile 里的 `qemu-system-riscv64 -machine virt …`,以及一个手工维护的 sysroot 路径。
32+
33+
---
34+
35+
## 场景 1:从零到一个会跑的固件
36+
37+
```bash
38+
mcpp new blinky --template riscv-virt-rt
39+
cd blinky
40+
mcpp run
41+
```
42+
43+
实际输出:
44+
45+
```
46+
Downloading mcpplibs.riscv-virt-rt v0.3.0
47+
Compiling blinky v0.1.0 (.)
48+
Finished dev [unoptimized + debuginfo] in 0.08s
49+
Size blinky text 8844 data 80 bss 5668 total 14592
50+
Running `…/qemu-system-riscv64 … target/riscv64-none-elf/…/bin/blinky`
51+
52+
hello from blinky
53+
float 3.1416
54+
heap ok
55+
```
56+
57+
生成的 `src/main.cpp`**普通的 `int main()`**:
58+
59+
```cpp
60+
import mcpplibs.riscv_virt_rt;
61+
62+
extern "C" int main() {
63+
board::println("hello from blinky");
64+
board::printf("float %.4f\n", 3.14159); // 浮点 printf 也能用
65+
void* p = board::alloc(64); // 堆也在
66+
board::println(p ? "heap ok" : "heap FAILED");
67+
board::release(p);
68+
return p ? 0 : 1; // 返回值经 semihosting 回到宿主
69+
}
70+
```
71+
72+
⚠️ **不需要 `_start`,不需要汇编入口**。板级包带了 picolibc 的 semihosting `crt0`,
73+
所以 C 运行时在 `main` 之前就已经起来了。只有**零 libc** 的板子才需要自己写入口。
74+
75+
---
76+
77+
## 场景 2:在目标上跑测试
78+
79+
```bash
80+
mcpp test
81+
```
82+
83+
```
84+
Compiling boots (test)
85+
Running bin/boots
86+
boots: console
87+
boots ... ok (0.04s)
88+
89+
test result ok. 1 passed; 0 failed; finished in 0.34s
90+
```
91+
92+
**每个 `tests/*.cpp` 是一个独立镜像,在模拟器里跑,退出码即判据** ——
93+
和宿主上的 `mcpp test` 完全一样的心智模型。
94+
95+
失败会**点名**:
96+
97+
```
98+
ok_one ... ok
99+
ok_two ... ok
100+
deliberate_fail ... FAIL (exit 1, 0.02s)
101+
error: test result: FAILED. 2 passed; 1 failed
102+
```
103+
104+
⚠️ 这条不是设计出来的,是**实测**出来的:semihosting 把固件 `main` 的返回值原样传给
105+
qemu 退出码(`return 7` → qemu 退出 7)。原计划要为裸机造一套结构化 stdout 协议,不需要。
106+
107+
---
108+
109+
## 场景 3:换 ISA 宽度 —— 改一个 flag
110+
111+
```bash
112+
mcpp build --target riscv32-none-elf
113+
mcpp run --target riscv32-none-elf
114+
```
115+
116+
**源码一个字不改,板级包一个字不改。**
117+
118+
```
119+
Size blinky text 10680 data 44 bss 5400 total 16124
120+
hello from blinky
121+
float 3.1416
122+
heap ok
123+
```
124+
125+
同一个板级包用一份描述服务两个宽度:它从 `MCPP_TARGET_ARCH` 选档位,
126+
而 ISA 参数(`-march`/`-mabi`/`-mcmodel`)来自引擎的目标表 —— **是数据不是代码**
127+
128+
---
129+
130+
## 场景 4:烧到真硬件要的东西
131+
132+
```bash
133+
mcpp build
134+
```
135+
136+
```
137+
Size blinky text 8844 data 80 bss 5668 total 14592
138+
```
139+
140+
产物旁边直接就有:
141+
142+
```
143+
target/riscv64-none-elf/<fp>/bin/blinky # ELF(调试 / qemu -kernel)
144+
target/riscv64-none-elf/<fp>/bin/blinky.bin # 裸二进制(烧录器只吃这个)
145+
target/riscv64-none-elf/<fp>/bin/blinky.map # 链接映射
146+
```
147+
148+
**size 摘要是每次构建都打印的**,因为裸机的核心约束是**容量** ——
149+
不打印等于让用户自己去查一个每次都想知道的数。
150+
151+
`.map` 是「为什么这段没进来 / 为什么这段这么大」唯一能回答的东西。
152+
153+
---
154+
155+
## 场景 5:在已有工程里加板级支持
156+
157+
```bash
158+
mcpp add riscv-virt-rt@0.3.0
159+
```
160+
161+
```toml
162+
[build]
163+
target = "riscv64-none-elf"
164+
165+
[dependencies]
166+
riscv-virt-rt = "0.3.0"
167+
```
168+
169+
**模拟器和目标 C 库会被自动装上** —— 用户不需要事先 `xlings install` 任何东西。
170+
(实测判据:把 store 里的 picolibc 藏起来,`mcpp add` + `mcpp build` 把它装了回来。)
171+
172+
---
173+
174+
## 场景 6:用标准库的可移植子集
175+
176+
```toml
177+
[dependencies]
178+
riscv-virt-rt = "0.3.0"
179+
std-freestanding = "0.2.0"
180+
```
181+
182+
```cpp
183+
import mcpplibs.riscv_virt_rt;
184+
import mcpplibs.std.freestanding; // 不是 `import std;`
185+
186+
struct Task { int prio; const char* name; };
187+
188+
extern "C" int main() {
189+
std::array<Task, 4> t{{ {3,"c"}, {1,"a"}, {4,"d"}, {2,"b"} }};
190+
std::ranges::sort(t, {}, &Task::prio); // 带投影,裸机上跑
191+
std::optional<int> o = 41;
192+
std::atomic<int> a{0};
193+
a.fetch_add(o.value() + 1);
194+
std::span<Task> s{t};
195+
std::string_view sv{"ok"};
196+
board::printf("atomic %d\n", a.load());
197+
return 0;
198+
}
199+
```
200+
201+
实测输出 `abcd` / `atomic 42` / `span 4`。
202+
203+
**可用**:`array` `span` `optional` `expected` `atomic` `string_view` `ranges` `algorithm`
204+
`bit` `charconv` `concepts` `type_traits` `tuple` `utility` coroutines …
205+
(libc++ 110 个头里的 **103** 个)
206+
207+
**干净地不可用**(编译期报错,不是跑起来才错):
208+
209+
```cpp
210+
std::mutex m; // error: no type named 'mutex' in namespace 'std'
211+
```
212+
213+
⚠️ **需要目标版 `libc++.a` 才有的**(今天会在**链接期**失败并点名符号):
214+
`std::format` · 内建标量类型的 `std::sort` · 完整的 `std::string`
215+
216+
### 如果直接写 `import std;` 会怎样
217+
218+
```
219+
error: `import std;` is not available on 'riscv64-none-elf' — a freestanding
220+
target has no hosted standard library.
221+
`std` is one module over the entire library (threads, filesystem,
222+
iostreams included), so there is no subset of it to build without an OS.
223+
Use the freestanding subset instead — an ordinary dependency carrying
224+
the parts of the library that need no OS (array, span, optional, atomic,
225+
string_view, ranges, expected, charconv, coroutines):
226+
227+
[dependencies]
228+
std-freestanding = "0.2.0"
229+
230+
then `import mcpplibs.std.freestanding;` in place of `import std;`.
231+
```
232+
233+
**诊断给的那一行是能直接粘贴并跑通的** —— 这是一条硬规矩:
234+
诊断里的每条建议都是承诺(本轮曾违反过一次,见 review §3.4)。
235+
236+
---
237+
238+
## 场景 7:调试时换掉板级包给的 runner
239+
240+
板级包供 runner 是常态,但工程可以覆盖:
241+
242+
```toml
243+
[target.riscv64-none-elf]
244+
runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
245+
"-bios", "default", # 换成 OpenSBI 启动
246+
"-s", "-S", # 挂 gdb,停在第一条指令
247+
"-kernel"]
248+
```
249+
250+
```
251+
note: [target.riscv64-none-elf].runner overrides the runner a dependency supplied
252+
```
253+
254+
⭐ 工程写的**赢过**依赖供的,而且 mcpp 会**说出来** —— 覆盖生效时不沉默。
255+
256+
---
257+
258+
## 场景 8:给一块新板子写 BSP(伪代码)
259+
260+
这是生态作者面。**整个 `build.mcpp` 大约 30 行**:
261+
262+
```cpp
263+
import mcpp;
264+
import std;
265+
266+
int main() {
267+
const bool rv32 = std::string_view{mcpp::target_arch() ?: ""} == "riscv32";
268+
269+
// 1. 从目标的 C 库里「选」—— 裸名即可,搜索路径引擎已经给了
270+
mcpp::link_lib("crt0-semihost"); // 换 UART 板就换成别的 crt0
271+
mcpp::link_lib("c");
272+
mcpp::link_lib("semihost");
273+
mcpp::link_lib(rv32 ? "clang_rt.builtins-riscv32"
274+
: "clang_rt.builtins-riscv64");
275+
276+
// 2. 这块板子的内存布局
277+
if (const char* sr = mcpp::sysroot_dir(); sr && *sr)
278+
mcpp::link_script(std::format("{}/lib/{}/picolibcpp.ld", sr,
279+
rv32 ? "rv32imac/ilp32" : "rv64gc/lp64d").c_str());
280+
281+
// 3. 怎么把镜像跑起来
282+
if (const char* q = mcpp::xpkg_dir("xim", "qemu-riscv"); q && *q) {
283+
mcpp::runner(std::format("{}/bin/qemu-system-{}", q,
284+
rv32 ? "riscv32" : "riscv64").c_str());
285+
for (auto a : {"-machine","virt","-nographic","-no-reboot",
286+
"-semihosting","-bios","none","-kernel"})
287+
mcpp::runner(a);
288+
}
289+
return 0;
290+
}
291+
```
292+
293+
⚠️ **注意它不做什么**:不找 libc、不声明 libc、不知道 libc 叫什么。
294+
它只知道**要哪个 crt0、要哪份链接脚本、怎么起模拟器** ——
295+
**位置是目标的事实,选择是板级的事实。**
296+
297+
换一块同 ISA 的板子 = 换这三段里的具体取值,**引擎零改动**
298+
299+
---
300+
301+
## 场景 9:老版本 mcpp 上会看到什么
302+
303+
```
304+
error: 'runner' is not a member of 'mcpp'
305+
The `mcpp` build module this engine bundles does not have that name.
306+
Either the package was written for a newer mcpp (try `mcpp self update`;
307+
this is mcpp 2026.8.19.1), or the name is misspelled — the compiler
308+
cannot tell the two apart, because the module is generated by whichever
309+
mcpp is running.
310+
```
311+
312+
⚠️ 这条提示是**补出来的**,因为包侧**无法**优雅降级:
313+
`if constexpr (requires { mcpp::runner("x"); })` 在名字不存在时是**硬错误**,不是 `false`
314+
—— 语言内没有特性探测这条路。所以引擎在编译失败时把「可能是引擎旧了」说出来。
315+
316+
---
317+
318+
## 一句话:用户感受到的四件事
319+
320+
| | 之前 | 现在 |
321+
|---|---|---|
322+
| **起步** | 自己攒 linker script + start.S + qemu 命令行 | `mcpp new --template riscv-virt-rt``mcpp run` |
323+
| **测试** | 裸机基本不测,或自造一套协议 | `mcpp test`,退出码即判据,失败点名 |
324+
| **换宽度** | 改一堆 flag 和路径 | `--target riscv32-none-elf`,源码零改 |
325+
| **烧录** | 自己 objcopy、自己看 size | `.bin`/`.map` 自动产出,size 每次构建都打印 |
326+
327+
---
328+
329+
## 已知边界(不要许诺给用户)
330+
331+
| 边界 | 表现 |
332+
|---|---|
333+
| `std::format` / 标量 `std::sort` / 完整 `std::string` | **链接期**失败并点名符号 —— 需要为目标编 `libc++.a`(未发布) |
334+
| 异常与 RTTI | 整图关闭(裸机没有 unwinder);`try/catch` 编译期就不可用 |
335+
| 第二块板 / ARM Cortex-M | 未做;rv32 只是「ISA 表是数据」的证据 |
336+
| Windows arm64 宿主 | 装不上 `qemu-riscv`(上游无该资产),行为正确但会失败 |
337+
| 目标 C 库不可按工程覆盖 | `[target.X]``toolchain`/`linkage`/`runner`/`cxx_runtime`,**没有 `sysroot`** —— 想换 newlib today 做不到 |

.agents/docs/2026-08-20-pr455-459-freestanding-review.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
**时间**:2026-08-19 一日之内(#455 合入 → #459 合入),发布 `2026.8.19.1``2026.8.19.4`
88

9+
**配套**:用户面场景与伪代码见 [裸机使用场景](2026-08-20-baremetal-user-facing-scenarios.md)
10+
911
⚠️ **本文的定位是 review 而不是总结**:凡是「做了什么」都给出可复核的出处(PR 号、
1012
文件、命令),凡是「做错了什么」都写明**发现方式****当时为什么没看见**
1113
一日四个补丁版本本身就是一个信号,第 6 节专门分析它。
@@ -115,7 +117,38 @@ e2e/131 从**两侧**钉它:目标 C 头**必须**到达消费者,板级包**自
115117

116118
⚠️ **这个错误能存在两个版本,是因为它在装好的机器上完全不可见** —— 见 §5.2。
117119

118-
### 2.4 单一读取点
120+
### 2.4 包命名与归属:两条都符合既有先例(review 时被问到)
121+
122+
**`picolibc-riscv` / `qemu-riscv` 把 target 写进包名,对吗?** —— 对,而且是 xim 的既有规则:
123+
124+
```
125+
aarch64-linux-musl-gcc riscv64-linux-musl-gcc x86_64-linux-musl-gcc
126+
mingw-cross-gcc mingw-w64 musl-gcc
127+
picolibc-riscv qemu-riscv
128+
```
129+
130+
**名字承载 TARGET,`archs` 轴承载 HOST**(`llvm``archs = {x86_64, arm64}` 是宿主)。
131+
`llvm` 名字里没有 target,因为它一份载荷服务所有 target —— 规则是一致的,不是巧合。
132+
133+
粒度落在 **ISA 家族**而不是 triple:`riscv64-none-elf``riscv32-none-elf` 共用一个
134+
`picolibc-riscv`(内含两个 multilib 档位),这正是 picolibc 上游一次构建的产物单位。
135+
136+
**picolibc 该在 mcpp-index 侧还是 xim 侧?** —— **xim 侧,构建期由目标行解析**,也就是 #459 的做法。
137+
138+
| 判据 | picolibc |
139+
|---|---|
140+
|`import`| ❌ 没有 C++ 模块,是头 + `libc.a` + crt0 + 链接脚本 |
141+
| 进依赖图吗 | ❌ 由 **target** 选定,不由用户的依赖图选定 |
142+
| 生态里同类的东西在哪 | `glibc` `musl` `musl-cross-make` **全在 xim**;`mcpp-index` 里 libc 包 **0 个** |
143+
144+
⇒ 放进 mcpp-index 会把 libc 重新塞回包依赖图,**正好撤销 #459**
145+
146+
⚠️ **但确实缺一个旋钮**:`TargetEntry` 今天有 `toolchain`/`linkage`/`runner`/`cxxRuntime`,
147+
**没有 `sysroot`** ⇒ 工程无法把 picolibc 换成 newlib。
148+
自然形态是 `[target.X] sysroot = "xim:newlib-riscv@…"`,与 `[toolchain]` 覆盖编译器同构。
149+
**未实现,列入 §8。**
150+
151+
### 2.5 单一读取点
119152

120153
`choose_runner(ctx)` 是「产物怎么执行」的**唯一**读取点,`mcpp run``mcpp test` 共用。
121154
这是本仓库反复付过学费的形状(#233/#240/#242/#344:同一决策两处推导)。
@@ -290,6 +323,7 @@ error: exception handling was enabled in precompiled file
290323
|---|---|---|
291324
| **T3 档**(`std::format`、标量 `std::sort`、完整 `std::string`) | 真实边界 | libc++ 把这些实体放在编译版库里(标量 `__sort` 是 `extern template`,**无宏可关**)⇒ 需为目标编 `libc++.a`,是新载荷 |
292325
| `mcpp-index` #220 | 待发布生效 | 依赖 2026.8.19.4 |
326+
| **`[target.X] sysroot` 覆盖** | 缺口 | 见 §2.4:今天换不了 libc 实现 |
293327
| 目标表**索引化** | 已定为阶段二 | 把 `pin` + `sysroot` 搬到索引,含老客户端降级路径;阶段一不会让它更难做 |
294328
| 第二块板 / ARM Cortex-M | 未开始 | rv32 已作为「ISA 表是数据」的证据 |
295329
| `prepare.cppm` / `execute.cppm` 体量 | 技术债 | 见 §4 |

0 commit comments

Comments
 (0)