|
| 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 做不到 | |
0 commit comments