Skip to content

Commit fdd6862

Browse files
docs: review of #455-#459, the openkal design, and the openkal 0.1 implementation plan (#461)
* 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. * 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. * docs: openkal design — a two-sided kernel ABI specification Written from the discussion that followed the #455-#459 review, and grounded in what is measurable today rather than sketched: every claim marked with a source was verified against the shipped payloads (llvm 22.1.8, gcc 16.1.0, picolibc 1.8.12) while writing. The load-bearing decisions: * Partitioning by RESOURCE KIND, not by which standard-library facility it lights up. The latter couples a kernel ABI to C++ and to today's library, and inverts the dependency — it is the same mistake POSIX made for C and WASIp1 made for POSIX, one generation further on. 'Lights up std::X' is demoted to the upward admission criterion, which is where it belongs. * An opaque one-word handle, because that is what makes openkal indifferent to sitting above or below libc. An int fd forces it below (Windows needs a table); a FILE* forces it above. Measured: four backends store their native thing with zero bridging. * fs and net dissolve. Naming goes to openkal.namespace, and what it hands back is the same stream resource a file, a socket or a UART gives you. Cleaner than 'everything is a file', because naming failure and I/O failure end up in different interfaces. * core is abort + stream + memory. Memory is core because a bump allocator over a static arena is an IMPLEMENTATION, not an emulation — the test being whether a fake would make callers silently wrong, which is true of a clock that does not advance but not of an allocator that can fail. Also records the caps-can-lie problem with four defences ordered by strength, led by making unsupported operations unrepresentable in the type system rather than false in a bool — the same conclusion K1/K2 reached for the MMU. * docs(openkal): retract two decisions after review, and record six open questions Two things the design got wrong, both retracted with the reasoning that made them look right at the time: * `openkal.namespace` replacing fs and net. It violated this document's own §5.1 rule (a stream whose caps are the union of file and socket operations is precisely the 'present but useless' antipattern), it required every backend to carry a URI parser — which is an emulation layer by the downward admission criterion — and the WASIp2 precedent it cited was a misreading: WASIp2 separates resource KINDS and shares only the stream type. * Extending cfg() with capability predicates. That conclusion was about openarch's AddressSpace; going through openkal interface by interface, core has no semantic axis at all, and the triple already carries most of what cfg(mmu) would have. The design is now a zero-engine-change proposal. Also corrects the module wiring: `reexport` propagates DOWNSTREAM, so an interface package cannot use it to reach a backend the consumer chose. The backend reexports the interface instead, which is what that mechanism is for. Six open questions the draft did not take a position on, led by one that is measured rather than theoretical: picolibc's vfprintf references free, so routing operator new to kal_alloc while printf keeps picolibc's malloc puts two allocators on the same RAM. The spec has to require that kal_alloc be built over a libc allocator where one exists, not beside it. * docs(openkal): capabilities are ADL-probeable — the caps struct and its config file are gone The design's §4 rested on one measurement: `requires { mcpp::runner("x") }` is a hard error when the name is absent. The measurement was right; the quantifier in the conclusion was not. It is QUALIFIED names that cannot be probed — unqualified lookup through ADL is dependent inside a template and evaluates to false, exactly as wanted. Verified on llvm 22.1.8 against real C++20 modules, not headers, with all three behaviours holding at once: * backend present → the concept is true and the call resolves to it * backend absent → the concept is FALSE, so `if constexpr` degrades * backend absent, called anyway → a compile error carrying the spec's own wording, which is what 'build the diagnostic in' was asking for ⚠️ One trap worth the record: if the fallback overload returns the same type as the real one, the concept is true even with no backend — a requires-expression does not instantiate the body, so the static_assert never fires. The fallback must return a distinct type. Measured, after writing it the other way first. Consequences: the caps struct, the generated caps module and capabilities.toml are all deleted. A backend's module interface IS its capability declaration, so claim and implementation become the same artifact by construction and the whole 'caps can lie' problem shrinks from structural+behavioural to behavioural only. Nothing leaves mcpp.toml — backend selection stays a conditional dependency. * docs(openkal): the backend owns the interface module name, and consumers declare both Answers two review questions that turned out to be the same question. The draft had the application write `import openkal.uart;`. That is wrong — it pins the source to a backend, which is the one thing openkal exists to avoid. But it papered over a real constraint, now measured on mcpp 2026.8.19.4 with gcc 16.1.0: * a transitive dependency's module IS importable * ⚠️ but ADL does NOT reach a module the translation unit did not import ('seek' was not declared in this scope) So the backend's declarations must live in the module the app imports, which forces the backend to own the well-known name `openkal.stream` while the interface package provides `openkal.abi.stream`. Verified end to end: the app writes one import, names no backend, and ADL resolves to the backend's seek. ⚠️ And a trap worth the record: the interface module cannot be called `openkal.stream.abi` — the module graph reads the dots as hierarchy and ninja reports a self-cycle on openkal.stream.gcm. `openkal.abi.stream` is fine. On dependencies: two, not one. The backend alone would work, but declaring the contract is what lets the APPLICATION pin the contract version, turning a mismatch into a resolution error instead of a pile of signature errors at compile time. Same shape as embedded-hal plus a board crate. * docs(openkal): openkal IS the ABI, and the fragmentation risk is mechanically bounded Naming: the interface package is `openkal`, not `openkal-abi` — openkal is the specification, so saying it twice is noise. Two module names are still forced by the language (§4.3), but the qualifier now lands only where implementers see it: applications write `import openkal.stream;`, backend authors write `export import openkal.decl.stream;`. The backend owning the application-visible module name is the one real cost of that shape, and it is a fragmentation risk: a backend could put non-standard names into the standard module and applications would not notice. What bounds it is that most of the surface is not the backend's to touch — ⓘ measured, a backend redefining the interface's types is rejected outright: error: redeclaring 'struct kal::io_result@openkal.decl.stream' in module 'openkal.stream' conflicts with import so the only freedom left is adding overloads, and THAT is statically checkable: conformance diffs the module's exported name set — and signatures, since an `unsigned long` offset would still win ADL through a conversion — against the spec list. Vendor extensions must live under a different module name, which makes 'I used an extension' visible in the source. Second review pass adds two findings: the cardinality that matters is one implementation per INTERFACE rather than one backend per program (a program may take stream from one provider and memory from another), and the fallback overload is too greedy — unconstrained, it catches every kal type and tells a socket it is not a seekable stream. * docs(openkal): add a complete Linux reference implementation Two identities: a backend that works today, and the thing other implementers copy. It does not move the D0 gate — that gate is whether a THIRD PARTY writes a third backend — but it turns 'guess the shape and write the implementation' into 'write the implementation'. Writing it out surfaced three things the design document had not: * core operations need no ADL at all. They are declared `extern "C"` by the interface and defined by the backend; missing means a link error. The ADL mechanism serves optional capabilities only, which makes the common path simpler than the draft implied. * short writes are a spec question nobody had asked. ::write(2) may write less than requested, so openkal must choose: write-all-or-error (the loop lives once, in the backend) or allow short writes (every caller writes the loop — which is exactly where POSIX has tripped programs up for decades). * ⭐ it independently confirms the fs/net decomposition. On Linux, seekability is a property of the HANDLE, not of the backend — lseek succeeds on a file and returns ESPIPE on a pipe. If openkal.stream had seek, the Linux backend could not answer honestly: claiming it means always failing on pipes, which is precisely the 'present but useless' antipattern. Because §2.3 puts seek on openkal.fs's descriptor instead, the question does not arise. That last point is the strongest argument for writing a complete reference at all: it is the only way to find a decomposition error, and it finds it earlier than a conformance suite would. * docs(openkal): module naming is normative, and decl is not interchangeable with impl `openkal.impl.*` would be semantically backwards: that module belongs to the interface package and holds declarations — types, the extern "C" surface, the fallback overloads, the concepts. What an implementation provides is `openkal.<interface>` itself. The name has to be in the spec rather than left to taste, for three reasons that are all load-bearing: every backend must `export import` that exact name, so it is part of the contract; the guarantee that a backend cannot redefine the interface's types only holds while all backends import the SAME module; and conformance's exported-name diff needs to know which names came from the shared module. ⚠️ The rationale has to ship with the rule. A spec reader will naturally reach for `openkal.stream.decl` — the dotted extension — and that one was measured to produce a ninja self-cycle on openkal.stream.gcm. A rule without its reason sends the first implementer straight into it. Also records a simplification that was considered and rejected: one `openkal` module holding every interface's declarations. It costs a naming level but breaks per-interface independent versioning, and drags task/fs declarations into a backend that only provides streams. * docs: openkal 0.1 implementation plan, and the design document now points at the shipped packages openkal 0.1 exists as two published packages: mcpplibs/openkal carries the specification and the modules that declare it, and mcpplibs/openkal-linux is the reference implementation, maintained as the worked example other implementations follow. Both are mirrored, and the mirrored archives were verified byte-identical. The plan document records the task dependencies, the criteria applied to each decision, and what verification established. Two results are worth separating from the rest. Writing a complete reference implementation confirmed the decomposition independently of the reasoning that produced it: on Linux, whether a stream can be repositioned is a property of the individual descriptor rather than of the implementation, so an openkal.stream that offered positioning could have been neither claimed honestly nor withheld usefully. A decomposition error of that kind is invisible in specification text and would have surfaced later. The exported-surface checker required by clause 9.3 was verified in both directions, and the negative direction mattered: an earlier version of it was vacuous, comparing a set of C++ symbols that inline functions never emit. The design document is now marked as the record of derivation, including withdrawn proposals and their reasons, while the specification records only conclusions. Where they disagree the specification governs. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 0ad5b52 commit fdd6862

4 files changed

Lines changed: 2078 additions & 0 deletions
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 做不到 |

0 commit comments

Comments
 (0)