配套 PR #455–#459 review。 本文只讲用户敲什么、看到什么、不用写什么;机制在 review 里。
以下命令与输出都是实测的(mcpp 2026.8.19.4 + riscv-virt-rt 0.3.0),
不是设想的接口。
这是本轮最大的用户面变化。裸机工程的整个 manifest:
[package]
name = "blinky"
version = "0.1.0"
[build]
target = "riscv64-none-elf"
[dependencies]
riscv-virt-rt = "0.3.0"⭐ 里面没有:链接脚本路径 · 加载地址 · -nostdlib · -march/-mabi/-mcmodel ·
crt0 · libc 名字 · 模拟器命令行 · [target.*] 段 —— 一个都没有。
对照:同样的工程在裸机 C/C++ 的常规做法里,通常需要一份 link.ld、一份 start.S、
一段 Makefile 里的 qemu-system-riscv64 -machine virt …,以及一个手工维护的 sysroot 路径。
mcpp new blinky --template riscv-virt-rt
cd blinky
mcpp run实际输出:
Downloading mcpplibs.riscv-virt-rt v0.3.0
Compiling blinky v0.1.0 (.)
Finished dev [unoptimized + debuginfo] in 0.08s
Size blinky text 8844 data 80 bss 5668 total 14592
Running `…/qemu-system-riscv64 … target/riscv64-none-elf/…/bin/blinky`
hello from blinky
float 3.1416
heap ok
生成的 src/main.cpp 是普通的 int main():
import mcpplibs.riscv_virt_rt;
extern "C" int main() {
board::println("hello from blinky");
board::printf("float %.4f\n", 3.14159); // 浮点 printf 也能用
void* p = board::alloc(64); // 堆也在
board::println(p ? "heap ok" : "heap FAILED");
board::release(p);
return p ? 0 : 1; // 返回值经 semihosting 回到宿主
}_start,不需要汇编入口。板级包带了 picolibc 的 semihosting crt0,
所以 C 运行时在 main 之前就已经起来了。只有零 libc 的板子才需要自己写入口。
mcpp test Compiling boots (test)
Running bin/boots
boots: console
boots ... ok (0.04s)
test result ok. 1 passed; 0 failed; finished in 0.34s
⭐ 每个 tests/*.cpp 是一个独立镜像,在模拟器里跑,退出码即判据 ——
和宿主上的 mcpp test 完全一样的心智模型。
失败会点名:
ok_one ... ok
ok_two ... ok
deliberate_fail ... FAIL (exit 1, 0.02s)
error: test result: FAILED. 2 passed; 1 failed
main 的返回值原样传给
qemu 退出码(return 7 → qemu 退出 7)。原计划要为裸机造一套结构化 stdout 协议,不需要。
mcpp build --target riscv32-none-elf
mcpp run --target riscv32-none-elf源码一个字不改,板级包一个字不改。
Size blinky text 10680 data 44 bss 5400 total 16124
hello from blinky
float 3.1416
heap ok
同一个板级包用一份描述服务两个宽度:它从 MCPP_TARGET_ARCH 选档位,
而 ISA 参数(-march/-mabi/-mcmodel)来自引擎的目标表 —— 是数据不是代码。
mcpp build Size blinky text 8844 data 80 bss 5668 total 14592
产物旁边直接就有:
target/riscv64-none-elf/<fp>/bin/blinky # ELF(调试 / qemu -kernel)
target/riscv64-none-elf/<fp>/bin/blinky.bin # 裸二进制(烧录器只吃这个)
target/riscv64-none-elf/<fp>/bin/blinky.map # 链接映射
⭐ size 摘要是每次构建都打印的,因为裸机的核心约束是容量 —— 不打印等于让用户自己去查一个每次都想知道的数。
.map 是「为什么这段没进来 / 为什么这段这么大」唯一能回答的东西。
mcpp add riscv-virt-rt@0.3.0[build]
target = "riscv64-none-elf"
[dependencies]
riscv-virt-rt = "0.3.0"⭐ 模拟器和目标 C 库会被自动装上 —— 用户不需要事先 xlings install 任何东西。
(实测判据:把 store 里的 picolibc 藏起来,mcpp add + mcpp build 把它装了回来。)
[dependencies]
riscv-virt-rt = "0.3.0"
std-freestanding = "0.2.0"import mcpplibs.riscv_virt_rt;
import mcpplibs.std.freestanding; // 不是 `import std;`
struct Task { int prio; const char* name; };
extern "C" int main() {
std::array<Task, 4> t{{ {3,"c"}, {1,"a"}, {4,"d"}, {2,"b"} }};
std::ranges::sort(t, {}, &Task::prio); // 带投影,裸机上跑
std::optional<int> o = 41;
std::atomic<int> a{0};
a.fetch_add(o.value() + 1);
std::span<Task> s{t};
std::string_view sv{"ok"};
board::printf("atomic %d\n", a.load());
return 0;
}实测输出 abcd / atomic 42 / span 4。
可用:array span optional expected atomic string_view ranges algorithm
bit charconv concepts type_traits tuple utility coroutines …
(libc++ 110 个头里的 103 个)
干净地不可用(编译期报错,不是跑起来才错):
std::mutex m; // error: no type named 'mutex' in namespace 'std'libc++.a 才有的(今天会在链接期失败并点名符号):
std::format · 内建标量类型的 std::sort · 完整的 std::string。
error: `import std;` is not available on 'riscv64-none-elf' — a freestanding
target has no hosted standard library.
`std` is one module over the entire library (threads, filesystem,
iostreams included), so there is no subset of it to build without an OS.
Use the freestanding subset instead — an ordinary dependency carrying
the parts of the library that need no OS (array, span, optional, atomic,
string_view, ranges, expected, charconv, coroutines):
[dependencies]
std-freestanding = "0.2.0"
then `import mcpplibs.std.freestanding;` in place of `import std;`.
⭐ 诊断给的那一行是能直接粘贴并跑通的 —— 这是一条硬规矩: 诊断里的每条建议都是承诺(本轮曾违反过一次,见 review §3.4)。
板级包供 runner 是常态,但工程可以覆盖:
[target.riscv64-none-elf]
runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
"-bios", "default", # 换成 OpenSBI 启动
"-s", "-S", # 挂 gdb,停在第一条指令
"-kernel"]note: [target.riscv64-none-elf].runner overrides the runner a dependency supplied
⭐ 工程写的赢过依赖供的,而且 mcpp 会说出来 —— 覆盖生效时不沉默。
这是生态作者面。整个 build.mcpp 大约 30 行:
import mcpp;
import std;
int main() {
const bool rv32 = std::string_view{mcpp::target_arch() ?: ""} == "riscv32";
// 1. 从目标的 C 库里「选」—— 裸名即可,搜索路径引擎已经给了
mcpp::link_lib("crt0-semihost"); // 换 UART 板就换成别的 crt0
mcpp::link_lib("c");
mcpp::link_lib("semihost");
mcpp::link_lib(rv32 ? "clang_rt.builtins-riscv32"
: "clang_rt.builtins-riscv64");
// 2. 这块板子的内存布局
if (const char* sr = mcpp::sysroot_dir(); sr && *sr)
mcpp::link_script(std::format("{}/lib/{}/picolibcpp.ld", sr,
rv32 ? "rv32imac/ilp32" : "rv64gc/lp64d").c_str());
// 3. 怎么把镜像跑起来
if (const char* q = mcpp::xpkg_dir("xim", "qemu-riscv"); q && *q) {
mcpp::runner(std::format("{}/bin/qemu-system-{}", q,
rv32 ? "riscv32" : "riscv64").c_str());
for (auto a : {"-machine","virt","-nographic","-no-reboot",
"-semihosting","-bios","none","-kernel"})
mcpp::runner(a);
}
return 0;
}换一块同 ISA 的板子 = 换这三段里的具体取值,引擎零改动。
error: 'runner' is not a member of 'mcpp'
The `mcpp` build module this engine bundles does not have that name.
Either the package was written for a newer mcpp (try `mcpp self update`;
this is mcpp 2026.8.19.1), or the name is misspelled — the compiler
cannot tell the two apart, because the module is generated by whichever
mcpp is running.
if constexpr (requires { mcpp::runner("x"); }) 在名字不存在时是硬错误,不是 false
—— 语言内没有特性探测这条路。所以引擎在编译失败时把「可能是引擎旧了」说出来。
| 之前 | 现在 | |
|---|---|---|
| 起步 | 自己攒 linker script + start.S + qemu 命令行 | mcpp new --template riscv-virt-rt → mcpp run |
| 测试 | 裸机基本不测,或自造一套协议 | mcpp test,退出码即判据,失败点名 |
| 换宽度 | 改一堆 flag 和路径 | --target riscv32-none-elf,源码零改 |
| 烧录 | 自己 objcopy、自己看 size | .bin/.map 自动产出,size 每次构建都打印 |
| 边界 | 表现 |
|---|---|
std::format / 标量 std::sort / 完整 std::string |
链接期失败并点名符号 —— 需要为目标编 libc++.a(未发布) |
| 异常与 RTTI | 整图关闭(裸机没有 unwinder);try/catch 编译期就不可用 |
| 第二块板 / ARM Cortex-M | 未做;rv32 只是「ISA 表是数据」的证据 |
| Windows arm64 宿主 | 装不上 qemu-riscv(上游无该资产),行为正确但会失败 |
| 目标 C 库不可按工程覆盖 | [target.X] 有 toolchain/linkage/runner/cxx_runtime,没有 sysroot —— 想换 newlib today 做不到 |