From f3e4ec9c6fc9a67f07608eaf9a32346c7bc85857 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:11:40 +0800 Subject: [PATCH 01/10] docs: deep review of #455-#459 and the bare-metal ecosystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...026-08-20-pr455-459-freestanding-review.md | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 .agents/docs/2026-08-20-pr455-459-freestanding-review.md diff --git a/.agents/docs/2026-08-20-pr455-459-freestanding-review.md b/.agents/docs/2026-08-20-pr455-459-freestanding-review.md new file mode 100644 index 00000000..c38cfefa --- /dev/null +++ b/.agents/docs/2026-08-20-pr455-459-freestanding-review.md @@ -0,0 +1,323 @@ +# PR #455–#459 深度 review:裸机 freestanding 从「能编」到「能用」 + +**范围**:`mcpp-community/mcpp` #455 · #456 · #457 · #458 · #459,以及同期的 +`openxlings/xim-pkgindex` #651/#652/#653(+ 三条版本 bump)、`mcpplibs/mcpp-index` +#219/#220、两个新建仓库 `mcpplibs/riscv-virt-rt` 与 `mcpplibs/std-freestanding`。 + +**时间**:2026-08-19 一日之内(#455 合入 → #459 合入),发布 `2026.8.19.1` … `2026.8.19.4`。 + +⚠️ **本文的定位是 review 而不是总结**:凡是「做了什么」都给出可复核的出处(PR 号、 +文件、命令),凡是「做错了什么」都写明**发现方式**与**当时为什么没看见**。 +一日四个补丁版本本身就是一个信号,第 6 节专门分析它。 + +--- + +## 0. 一句话结论 + +引擎侧的形状是对的,而且**收敛得比计划更小**;真正的成本几乎全在 +**「我把属于 target 的东西写成了包的依赖」** 这一类边界判断上 —— +五个已修缺陷里有三个是同一个根因的不同表现。 + +判据(用**已发布**二进制、从零验证): + +```bash +mcpp new blinky --template riscv-virt-rt +cd blinky && mcpp run # qemu 里打印;manifest 里没有 [target.*] 段 +``` + +```toml +[package] name = "blinky" / version = "0.1.0" +[build] target = "riscv64-none-elf" +[dependencies] riscv-virt-rt = "0.3.0" +``` + +--- + +## 1. 五个 PR 各做了什么 + +| PR | 规模 | 内容 | 版本 | +|---|---|---|---| +| **#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 | +| **#456** | +2/−2 | 发布 | — | +| **#457** | +1388/−49,24 文件 | **runner 归 BSP**(`mcpp:runner=`,新 `Slot::Runner` + `Scope::RunGlobal`,protocol 4)、裸机 `mcpp test`、产物集(`.bin`/`.map`/size 摘要) | 2026.8.19.2 | +| **#458** | +109/−3,5 文件 | 修 `mcpp build` 之后 `mcpp run` 在宿主上直接 exec 裸机 ELF | 2026.8.19.3 | +| **#459** | +753/−45,21 文件 | 异常/RTTI 整图关闭、**目标自带 sysroot 轴**、`toolchain_dir`/`sysroot_dir`、依赖缓存键补 target flags、std 子集打通 | 2026.8.19.4 | + +累计(从 #455 之前的 `9c5cc8d` 算起,含同期其它工作):**87 文件 / +10330 / −161**。 +其中 `src/freestanding/` 三个模块共 **432 行**,裸机 e2e 四个脚本共 **983 行**, +`test_freestanding.cpp` **25 个单测**。 + +**指令表 13 → 15 行,protocol 2 → 4。** 两条新指令(`link-script`、`runner`)各自 +只占表里一行 —— 这是 #455/#457 最值得记的一点,见 §2.1。 + +### 生态侧 + +| 仓库 | 内容 | +|---|---| +| `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 | +| `mcpplibs/mcpp-index` | #219 收录 `riscv-virt-rt`(已合)· #220 `std-freestanding 0.2.0` + `riscv-virt-rt 0.3.0`(待 2026.8.19.4 生效) | +| `mcpplibs/riscv-virt-rt` | 板级支持包,0.1.0 → **0.3.0**;含 `templates/blinky`(`mcpp new --template`)、examples、仓库 CI(rv64+rv32 真跑 qemu) | +| `mcpplibs/std-freestanding` | freestanding std 子集,0.1.0 → **0.2.0**;`tools/regenerate.sh` 实测生成 | + +两个包 GitHub + GitCode 双镜像,tarball **逐字节校验一致**。 + +--- + +## 2. 架构评估 + +### 2.1 ⭐ 最好的一条:能力增长没有换来概念增长 + +裸机需要三样东西是引擎原本表达不了的:**链接脚本**、**执行方式**、**目标 C 库位置**。 +三样落地为: + +- `link-script` —— 指令表**一行**(`Scope::LinkGlobal`,`Transform::LinkerScript`); +- `runner` —— 指令表**一行** + 一个新 `Slot` + 一个新 `Scope`; +- 目标 sysroot —— `TargetInfo` **一个字段**,与 `pin` 并列。 + +⚠️ **计划里原本有更大的方案,是实测把它们砍掉的**: + +| 计划写的 | 实测 | 结果 | +|---|---|---| +| W8「带序 startup-objects 具名槽」 | `-lcrt0-semihost` 从**归档**里拉得进来,顺序由链接脚本 section 序决定 | ⛔ 整个槽位机制不需要,真正缺的只有 `-T` | +| §4「裸机 `mcpp test` 要 batch 模式 + 结构化 stdout 协议」 | qemu 冷启动 **12ms**(不是计划写的 0.4s);semihosting **把固件 `main` 返回值原样传给 qemu 退出码** | ⛔ 六个设计决定里四个不必做,R3 缩成「走 `mcpp run` 用的同一个 runner」 | +| W12 `requires-hard` 能力硬门 | 同一个 token 要同时表达「平台本来没有」与「runner 配错了」 | ⛔ 实现后用了一次,macOS e2e **一小时内证伪**,整个机制撤销 | + +⇒ **这三条是本轮质量的主要来源**:计划里凡是带具体数字或「A 不成立所以要 B」的句子, +数字与断言本身就是必须先测的探针。 + +### 2.2 ⭐ 作用域的不对称是刻意的,而且被两侧钉住 + +`directives.cppm` 里 `link-search`/`link-lib`/`link-script` 是 **LinkGlobal**(到达消费者), +`include-dir`/`cflag`/`cfg` 是 **PackagePrivate**(不到达)。这条供应链纪律让 +**引擎完全不需要 sysroot 概念**就能让 BSP 工作:目标头由包私有 include、对外只 export 一个模块。 + +e2e/131 从**两侧**钉它:目标 C 头**必须**到达消费者,板级包**自己的**头**必须不**到达。 + +### 2.3 ⚠️ 最大的架构错误:把 target 的属性写成了包的依赖 + +`riscv-virt-rt` 与 `std-freestanding` 都曾声明 +`[xlings] deps = ["xim:picolibc-riscv@1.8.12"]`,后者还声明了 `xim:llvm`。 +**这是用户 review 时指出的**,三条都对:不该绑 libc、不该绑架构、不该绑编译器实现。 + +真因是**引擎的结构性缺口**,不是包写得随意: + +| | 编译器 | 目标 libc | +|---|---|---| +| hosted(`x86_64-linux-musl`) | 目标表 `pin` **自动** | musl 在 gcc 载荷里 / glibc 走 `PayloadPaths` —— **自动**,从没人写过 `xim:glibc` | +| freestanding | 目标表 `pin = llvm@22.1.8` **自动** | **一条轴都没有** ⇒ 外溢到每个包 | + +⇒ #459 给 `TargetInfo` 加 `sysroot` 轴,复用现有 `[xlings] deps` 物化通道安装, +引擎发 `-isystem /include/<档位>` 与 `-L /lib/<档位>`。 +再补两个「问引擎」的接口 `mcpp::toolchain_dir()` / `mcpp::sysroot_dir()`。 + +**结果**:`std-freestanding` 变成**零依赖**,`riscv-virt-rt` 只剩模拟器(那确实是板级事实)。 +分界线也随之清楚了:**位置是目标的事实,选择是板级的事实。** + +⚠️ **这个错误能存在两个版本,是因为它在装好的机器上完全不可见** —— 见 §5.2。 + +### 2.4 单一读取点 + +`choose_runner(ctx)` 是「产物怎么执行」的**唯一**读取点,`mcpp run` 与 `mcpp test` 共用。 +这是本仓库反复付过学费的形状(#233/#240/#242/#344:同一决策两处推导)。 +覆盖语义也留了:`[target.X].runner` 覆盖 BSP 供的,调试时换 `-bios` 是正当需求。 + +--- + +## 3. 兼容性评估 + +### 3.1 ⭐ 已发布数据不得让已发布程序失效 + +索引 `min_mcpp` **没有抬**(memory:`index-floor-must-degrade`)。 +新包进索引不会让老客户端拿不到任何包。`riscv-virt-rt` 0.1.0/0.2.0 **留在索引里**, +因为「已经发布过的版本就是可能有人 pin 过的版本」。 + +### 3.2 ⚠️ 类型化 API 没有语言内的特性探测(实测) + +```cpp +if constexpr (requires { mcpp::runner("x"); }) // 名字不存在 ⇒ 硬错误,不是 false +``` + +`requires` 表达式作用在**不存在的限定名**上是 ill-formed。⇒ 包**无法**优雅降级; +而 wire 协议那条点名 `mcpp self update` 的诊断也够不着(它要求程序**先编译得过**)。 + +⇒ 补在引擎侧:`build.mcpp` 编译失败且错误含「不是 `mcpp` 的成员」时追加升级提示, +三个前端三种拼写都认。**对以后每一次类型化 API 新增都生效**,不只是 `runner`。 + +### 3.3 ⚠️ 升级即坏的缓存缺陷(#459 修) + +依赖缓存键有 `targetTriple`,**没有 triple 隐含的那组 flag**。而**哪些 flag 由 triple 隐含 +是 mcpp 的决定**,会随版本变、triple 字符串不变。 + +⇒ 加 `-fno-exceptions` 那一刻,所有已建过的裸机工程**复用升级前的 BMI**,硬失败, +而错误只点名一个 `.pcm` 文件: + +``` +error: exception handling was enabled in precompiled file +'mcpplibs.riscv_virt_rt.pcm' but is currently disabled +``` + +已加 `targetImpliedFlags` 轴(hosted 为空,不动任何现有键),单测钉「加一个 flag 键必须变」。 + +### 3.4 ⚠️ 一条指向不存在的包的诊断(自造,已修) + +第二阶段的 `import std` 诊断末尾给了可直接粘贴的 +`mcpplibs.std.freestanding = "0.1"` —— **这个包当时没发布**。用户粘完下一条命令报 +`package not found`。⇒ **诊断里的每条建议都是承诺**;现在包发布了,那一行才放回去。 + +### 3.5 破坏性变更盘点 + +| 变更 | 影响面 | 处置 | +|---|---|---| +| `mcpp run` 位置参数改名 `target` → `bin` | **零**(只出现在 `--help`,`cmd_run` 按下标读) | — | +| `mcpp run --target-triple` | 保留为别名(2026.8.19.1 已发布的拼写),e2e/130 两个拼写都钉 | — | +| freestanding 加 `-fno-exceptions -fno-rtti` | 裸机工程全体 | 由 §3.3 的缓存键保证重编;**异常在裸机上本来就不可用**(无 unwinder) | +| 目标自带 sysroot | 老包的 `[xlings] deps` 仍可留(去重),**不破坏** | 新包不必写 | + +--- + +## 4. 简洁与优雅 + +**好的**: + +- `src/freestanding/` 三个模块 **432 行**承载全部裸机知识,且 `linkline` 是**纯字符串构造** + (不依赖 `CompileFlags`,避免模块环)。 +- ISA 表是**数据**:`riscv32` 支持靠表里加一行,引擎与 BSP 零改动 —— 这条被 N3 判据实测过。 +- 模板**随包走**:`cmd_new` 的内建注册表刻意冻结在 `bin`,其余是包模板; + `mcpp new` 把自依赖按**解析到的版本**写进 manifest ⇒ **模板不可能与库脱节**。 + ⚠️ 计划里原本要给 mcpp 加内建 `--template baremetal-riscv`,**是错的形状**。 +- std 子集**不手写导出表**:libc++ 自带 110 个 `std/
.inc`,子集是机械挑选, + 生成物 217 行全是 `#include`;`tools/regenerate.sh` 每次运行都打印宿主对照组。 + +**不够好的**: + +- ⚠️ `src/build/prepare.cppm` 与 `execute.cppm` 各 +136/+137 行。这两个文件已经很大, + 本轮又往里加了 sysroot 解析、runner 选择、size 报告、快路径前置条件。 + **它们正在变成「什么都知道的地方」**,下一轮应当先拆再加。 +- `build.mcpp` 里合成 `__config_site` 是 40 行字符串拼接。可用但不优雅; + 若第二个包也要合成配置,应当抽成 `mcpp::` 的一个接口。 + +--- + +## 5. 稳定性评估 + +### 5.1 已修缺陷清单(全部为本轮自造) + +| # | 缺陷 | 发现方式 | ⚠️ 当时为什么没看见 | +|---|---|---|---| +| 1 | `mcpp run ` 被 `--target` 吃掉 | **CI(e2e/73)** | 位置参数上**原本就有注释写着这个碰撞**,我为了「一致性」覆盖了它 | +| 2 | `mcpp build` 后 `mcpp run` 在宿主 exec 裸机 ELF | **发布后**用发布二进制走新用户流程 | 130/131/132 三个 e2e **都恰好先 `run` 后 `build`**,顺序本身是被测对象 | +| 3 | 诊断指向不存在的包 | 自查 | 写诊断时没验证那一行今天能跑通 | +| 4 | 包声明了 target 的 libc/编译器 | **用户 review** | 在已装好的机器上,这条边在与不在**完全一样** | +| 5 | 缓存键缺 target 隐含 flags | 实施 #459 时的升级路径自测 | 只有跨版本才暴露 | + +### 5.2 ⚠️ 三条「判据本身是错的」 + +这是本轮最值得记的一类问题 —— 测试是绿的,但它测的不是那件事。 + +1. **第一版 build-then-run 回归测试是假绿**:加进 e2e/131 的既有工程后, + **关掉修复它照样过**。给 `try_fast_run` 的 16 个 `return nullopt` 各打编号探针后定位到: + `mcpp.toml` 比 `build.ninja` 新,**快路径本来就没被走到**。 + 真因:**重建不会重写内容未变的 `build.ninja`** ⇒ 在原地编辑过 manifest 的目录里, + manifest 永远最新,快路径**永久关闭**。⇒ 这类测试必须**新建干净工程**,并先确认它**变红**。 +2. **e2e/131 的私有性断言判据失效**:它用「`#include ` 编不过」证明依赖的 + `include-dir` 不到达消费者。libc 归目标之后 `` **本来就该编得过**(和宿主一样)。 + **原来那条测的其实是「libc 从哪来」,不是「作用域对不对」。** +3. **「拿走再装回来」**:xim 安装期依赖边在**已装好的机器**上不可验证。 + 判据必须是把 store 里的包改名藏起来 → `mcpp add` + `mcpp build` 把它装回来。 + +### 5.3 ⚠️ `.map` 曾是「写了但没人跟踪」 + +`-Wl,-Map=` 是链接命令上的 flag,不是独立边,所以很容易只写不声明 —— +删掉 `firmware.map`,ninja 认为 ELF 是新的、什么都不做,map 就永远不回来。 +已声明为链接边的 implicit output,e2e/132 用「删掉必须回来」钉住(**这是唯一能在修复前失败的形式**)。 + +### 5.4 测试面 + +- 单测:`test_freestanding.cpp` 25 个 + `test_build_directives` 33 个 + cache_key 19 个。 +- e2e:130(引擎链)/131(生态链)/132(test + 产物)/133(std 子集),共 983 行。 +- CI:新增 `baremetal` job,**两个 home 都装 qemu**(shim 按拥有它的 home 派发), + 并且**断言每条 PASS 行真的出现** —— `run_all.sh` 跳过时退出码是 0。 +- 包侧 CI:`riscv-virt-rt` 与 `std-freestanding` 各自仓库 CI 真跑 qemu,rv64+rv32 双档。 + +--- + +## 6. ⚠️ 一日四个补丁版本:这个节奏说明什么 + +`2026.8.19.1` → `.2` → `.3` → `.4`,其中 `.3` 与 `.4` 都是在**发布之后**发现问题才发的。 + +**不是坏事的部分**:`.3` 的缺陷是靠「用发布二进制从零走一遍新用户流程」发现的 —— +这一步如果省掉,它会留在生态里直到有用户撞上。**这一步的价值被证明了。** + +**是问题的部分**: + +- `.2` 发布时,`mcpp build && mcpp run` 这条**新用户最可能敲的两条命令**没有被任何测试覆盖。 + 四个裸机 e2e 都先 `run`,是巧合而非设计。 +- `.4` 的架构问题(§2.3)是**用户 review 发现的**,不是我自查发现的。我在 `.3` 的收尾报告里 + 甚至把 std 子集写成「要从头实现、不该在收尾时半做」——**而调研文档 X1–X10 早就把这条路测到了底**。 + ⇒ **写「这条做不了」之前必须回查自己的调研文档。** + +**改进项**(下一轮的硬约束): + +1. 发布前必须跑一遍**命令序列矩阵**(`build→run`、`run→build`、`build→test`),而不是单条命令。 +2. 任何「某某做不了 / 代价很大」的结论,若同一课题有调研文档,**必须引用文档中的具体测量**才能下。 +3. 回归测试必须做 **revert-A 探针**(关掉修复看它是否变红),否则不算写完。 + +--- + +## 7. 跨平台评估 + +⭐ **裸机 target 反而是最省跨平台成本的一类**:clang/lld 是 cross-compiler by construction, +**任何能装 llvm 载荷的宿主都能产出 RISC-V 镜像**,不需要 per-host 交叉载荷。 +`host_can_serve` 对 freestanding 直接返回 `true`,这条在目标表注释里写明了。 + +| 轴 | 状态 | +|---|---| +| 宿主 | 引擎侧 21 个 CI job 全绿(linux/macOS/Windows/aarch64/hermetic/mingw-cross) | +| 模拟器 | `xim:qemu-riscv` 覆盖 **五个宿主目标**(linux x64/arm64 · darwin x64/arm64 · win32 x64) | +| 目标 sysroot | `picolibc-riscv` 与宿主无关(目标侧产物),三平台同一份 | +| ⚠️ 缺口 | `qemu-riscv` **win32-arm64 上游无资产** ⇒ 该宿主装不上,行为正确但会失败 | +| ⚠️ 缺口 | 两个包的仓库 CI **只有 ubuntu-24.04**;macOS/Windows 上的裸机链**没有持续验证** | + +⚠️ **mcpp-index 的 `tests/examples/` workspace 成员在三个平台上无条件跑,没有能力门** +⇒ 需要模拟器/目标 sysroot 的包**不能加成员**,验证只能放包自己的仓库 CI。 +这是一个**已知的覆盖缺口**,不是遗漏。 + +--- + +## 8. 仍然开着的 + +| 项 | 性质 | 备注 | +|---|---|---| +| **T3 档**(`std::format`、标量 `std::sort`、完整 `std::string`) | 真实边界 | libc++ 把这些实体放在编译版库里(标量 `__sort` 是 `extern template`,**无宏可关**)⇒ 需为目标编 `libc++.a`,是新载荷 | +| `mcpp-index` #220 | 待发布生效 | 依赖 2026.8.19.4 | +| 目标表**索引化** | 已定为阶段二 | 把 `pin` + `sysroot` 搬到索引,含老客户端降级路径;阶段一不会让它更难做 | +| 第二块板 / ARM Cortex-M | 未开始 | rv32 已作为「ISA 表是数据」的证据 | +| `prepare.cppm` / `execute.cppm` 体量 | 技术债 | 见 §4 | +| 包侧 CI 单平台 | 覆盖缺口 | 见 §7 | + +--- + +## 9. 分项评分 + +| 维度 | 评价 | 依据 | +|---|---|---| +| **架构** | 好 | 能力增长未换来概念增长(2 条指令 + 1 个字段);三个计划中的大机制被实测砍掉;单一读取点 | +| **兼容性** | 好,但有一次真实风险 | 索引底线未抬、别名保留、破坏面为零;⚠️ 但 `-fno-exceptions` 差点造成升级即坏,靠缓存键轴补上 | +| **简洁优雅** | 好 | 432 行承载全部裸机知识;ISA 表是数据;模板随包走;子集不手写导出表 | +| **稳定性** | ⚠️ 中 | 五个自造缺陷,两个是**发布后**才发现;三条判据本身是错的 | +| **跨平台** | 好,有已知缺口 | 引擎侧 21 job 全绿、模拟器五宿主;⚠️ 包侧 CI 单平台 | +| **一致性** | 好 | `--target` 在所有子命令上拼写一致(靠位置参数改名换来,而非放弃一致性) | +| **无感升级** | ⚠️ 中 | 缓存键轴与诊断升级提示都是**补出来的**,不是设计时就有的 | + +--- + +## 10. 三条要带进下一轮的 + +1. ⭐ **计划里的数字就是探针。** 本轮被实测推翻的计划主张:qemu 冷启动(0.4s→12ms)、 + 「裸机没有退出码」(semihosting 有)、builtins 缺口触发者(不是 64 位除法,是 128 位移位)、 + 「libc++ 头一个都用不了」(103/110)。**四条全是我没测就写下的。** +2. ⭐ **在「已经配置好的机器」上,很多边验不出来。** 目标 libc 的依赖边、xim 安装期依赖边、 + 快路径前置条件 —— 三者的共同点是**存在与不存在长得一模一样**。 + 判据必须是**拿走**(stash 后重装)或**新建**(干净工程),不能是「在我这儿是好的」。 +3. ⚠️ **顺序本身是被测对象。** `mcpp run` 对、`mcpp build && mcpp run` 错, + 而四个裸机 e2e 恰好都先 `run`。**命令序列矩阵要进发布前检查表。** From eee3e8e4e4754c6ad1d9ac1064ff4e0cec55db05 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:39:30 +0800 Subject: [PATCH 02/10] docs: user-facing bare-metal scenarios, and the package-naming/ownership answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...6-08-20-baremetal-user-facing-scenarios.md | 337 ++++++++++++++++++ ...026-08-20-pr455-459-freestanding-review.md | 36 +- 2 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 .agents/docs/2026-08-20-baremetal-user-facing-scenarios.md diff --git a/.agents/docs/2026-08-20-baremetal-user-facing-scenarios.md b/.agents/docs/2026-08-20-baremetal-user-facing-scenarios.md new file mode 100644 index 00000000..d06c4a11 --- /dev/null +++ b/.agents/docs/2026-08-20-baremetal-user-facing-scenarios.md @@ -0,0 +1,337 @@ +# 裸机:用户面能感受到的变化(场景 + 伪代码) + +配套 [PR #455–#459 review](2026-08-20-pr455-459-freestanding-review.md)。 +本文只讲**用户敲什么、看到什么、不用写什么**;机制在 review 里。 + +以下命令与输出都是**实测**的(mcpp 2026.8.19.4 + `riscv-virt-rt 0.3.0`), +不是设想的接口。 + +--- + +## 场景 0:先看「不用写什么」 + +这是本轮最大的用户面变化。裸机工程的**整个** manifest: + +```toml +[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 路径。 + +--- + +## 场景 1:从零到一个会跑的固件 + +```bash +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()`**: + +```cpp +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** 的板子才需要自己写入口。 + +--- + +## 场景 2:在目标上跑测试 + +```bash +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 +``` + +⚠️ 这条不是设计出来的,是**实测**出来的:semihosting 把固件 `main` 的返回值原样传给 +qemu 退出码(`return 7` → qemu 退出 7)。原计划要为裸机造一套结构化 stdout 协议,不需要。 + +--- + +## 场景 3:换 ISA 宽度 —— 改一个 flag + +```bash +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`)来自引擎的目标表 —— **是数据不是代码**。 + +--- + +## 场景 4:烧到真硬件要的东西 + +```bash +mcpp build +``` + +``` + Size blinky text 8844 data 80 bss 5668 total 14592 +``` + +产物旁边直接就有: + +``` +target/riscv64-none-elf//bin/blinky # ELF(调试 / qemu -kernel) +target/riscv64-none-elf//bin/blinky.bin # 裸二进制(烧录器只吃这个) +target/riscv64-none-elf//bin/blinky.map # 链接映射 +``` + +⭐ **size 摘要是每次构建都打印的**,因为裸机的核心约束是**容量** —— +不打印等于让用户自己去查一个每次都想知道的数。 + +`.map` 是「为什么这段没进来 / 为什么这段这么大」唯一能回答的东西。 + +--- + +## 场景 5:在已有工程里加板级支持 + +```bash +mcpp add riscv-virt-rt@0.3.0 +``` + +```toml +[build] +target = "riscv64-none-elf" + +[dependencies] +riscv-virt-rt = "0.3.0" +``` + +⭐ **模拟器和目标 C 库会被自动装上** —— 用户不需要事先 `xlings install` 任何东西。 +(实测判据:把 store 里的 picolibc 藏起来,`mcpp add` + `mcpp build` 把它装了回来。) + +--- + +## 场景 6:用标准库的可移植子集 + +```toml +[dependencies] +riscv-virt-rt = "0.3.0" +std-freestanding = "0.2.0" +``` + +```cpp +import mcpplibs.riscv_virt_rt; +import mcpplibs.std.freestanding; // 不是 `import std;` + +struct Task { int prio; const char* name; }; + +extern "C" int main() { + std::array t{{ {3,"c"}, {1,"a"}, {4,"d"}, {2,"b"} }}; + std::ranges::sort(t, {}, &Task::prio); // 带投影,裸机上跑 + std::optional o = 41; + std::atomic a{0}; + a.fetch_add(o.value() + 1); + std::span 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** 个) + +**干净地不可用**(编译期报错,不是跑起来才错): + +```cpp +std::mutex m; // error: no type named 'mutex' in namespace 'std' +``` + +⚠️ **需要目标版 `libc++.a` 才有的**(今天会在**链接期**失败并点名符号): +`std::format` · 内建标量类型的 `std::sort` · 完整的 `std::string`。 + +### 如果直接写 `import std;` 会怎样 + +``` +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)。 + +--- + +## 场景 7:调试时换掉板级包给的 runner + +板级包供 runner 是常态,但工程可以覆盖: + +```toml +[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 会**说出来** —— 覆盖生效时不沉默。 + +--- + +## 场景 8:给一块新板子写 BSP(伪代码) + +这是生态作者面。**整个 `build.mcpp` 大约 30 行**: + +```cpp +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; +} +``` + +⚠️ **注意它不做什么**:不找 libc、不声明 libc、不知道 libc 叫什么。 +它只知道**要哪个 crt0、要哪份链接脚本、怎么起模拟器** —— +**位置是目标的事实,选择是板级的事实。** + +换一块同 ISA 的板子 = 换这三段里的具体取值,**引擎零改动**。 + +--- + +## 场景 9:老版本 mcpp 上会看到什么 + +``` +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 做不到 | diff --git a/.agents/docs/2026-08-20-pr455-459-freestanding-review.md b/.agents/docs/2026-08-20-pr455-459-freestanding-review.md index c38cfefa..cea4e917 100644 --- a/.agents/docs/2026-08-20-pr455-459-freestanding-review.md +++ b/.agents/docs/2026-08-20-pr455-459-freestanding-review.md @@ -6,6 +6,8 @@ **时间**:2026-08-19 一日之内(#455 合入 → #459 合入),发布 `2026.8.19.1` … `2026.8.19.4`。 +**配套**:用户面场景与伪代码见 [裸机使用场景](2026-08-20-baremetal-user-facing-scenarios.md)。 + ⚠️ **本文的定位是 review 而不是总结**:凡是「做了什么」都给出可复核的出处(PR 号、 文件、命令),凡是「做错了什么」都写明**发现方式**与**当时为什么没看见**。 一日四个补丁版本本身就是一个信号,第 6 节专门分析它。 @@ -115,7 +117,38 @@ e2e/131 从**两侧**钉它:目标 C 头**必须**到达消费者,板级包**自 ⚠️ **这个错误能存在两个版本,是因为它在装好的机器上完全不可见** —— 见 §5.2。 -### 2.4 单一读取点 +### 2.4 包命名与归属:两条都符合既有先例(review 时被问到) + +**`picolibc-riscv` / `qemu-riscv` 把 target 写进包名,对吗?** —— 对,而且是 xim 的既有规则: + +``` +aarch64-linux-musl-gcc riscv64-linux-musl-gcc x86_64-linux-musl-gcc +mingw-cross-gcc mingw-w64 musl-gcc +picolibc-riscv qemu-riscv +``` + +⭐ **名字承载 TARGET,`archs` 轴承载 HOST**(`llvm` 的 `archs = {x86_64, arm64}` 是宿主)。 +`llvm` 名字里没有 target,因为它一份载荷服务所有 target —— 规则是一致的,不是巧合。 + +粒度落在 **ISA 家族**而不是 triple:`riscv64-none-elf` 与 `riscv32-none-elf` 共用一个 +`picolibc-riscv`(内含两个 multilib 档位),这正是 picolibc 上游一次构建的产物单位。 + +**picolibc 该在 mcpp-index 侧还是 xim 侧?** —— **xim 侧,构建期由目标行解析**,也就是 #459 的做法。 + +| 判据 | picolibc | +|---|---| +| 可 `import` 吗 | ❌ 没有 C++ 模块,是头 + `libc.a` + crt0 + 链接脚本 | +| 进依赖图吗 | ❌ 由 **target** 选定,不由用户的依赖图选定 | +| 生态里同类的东西在哪 | `glibc` `musl` `musl-cross-make` **全在 xim**;`mcpp-index` 里 libc 包 **0 个** | + +⇒ 放进 mcpp-index 会把 libc 重新塞回包依赖图,**正好撤销 #459**。 + +⚠️ **但确实缺一个旋钮**:`TargetEntry` 今天有 `toolchain`/`linkage`/`runner`/`cxxRuntime`, +**没有 `sysroot`** ⇒ 工程无法把 picolibc 换成 newlib。 +自然形态是 `[target.X] sysroot = "xim:newlib-riscv@…"`,与 `[toolchain]` 覆盖编译器同构。 +**未实现,列入 §8。** + +### 2.5 单一读取点 `choose_runner(ctx)` 是「产物怎么执行」的**唯一**读取点,`mcpp run` 与 `mcpp test` 共用。 这是本仓库反复付过学费的形状(#233/#240/#242/#344:同一决策两处推导)。 @@ -290,6 +323,7 @@ error: exception handling was enabled in precompiled file |---|---|---| | **T3 档**(`std::format`、标量 `std::sort`、完整 `std::string`) | 真实边界 | libc++ 把这些实体放在编译版库里(标量 `__sort` 是 `extern template`,**无宏可关**)⇒ 需为目标编 `libc++.a`,是新载荷 | | `mcpp-index` #220 | 待发布生效 | 依赖 2026.8.19.4 | +| **`[target.X] sysroot` 覆盖** | 缺口 | 见 §2.4:今天换不了 libc 实现 | | 目标表**索引化** | 已定为阶段二 | 把 `pin` + `sysroot` 搬到索引,含老客户端降级路径;阶段一不会让它更难做 | | 第二块板 / ARM Cortex-M | 未开始 | rv32 已作为「ISA 表是数据」的证据 | | `prepare.cppm` / `execute.cppm` 体量 | 技术债 | 见 §4 | From 708e70f3c012c5bef2c75a59469cdad30e5a6e9d Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:56:42 +0800 Subject: [PATCH 03/10] =?UTF-8?q?docs:=20openkal=20design=20=E2=80=94=20a?= =?UTF-8?q?=20two-sided=20kernel=20ABI=20specification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/docs/2026-08-20-openkal-design.md | 609 ++++++++++++++++++++++ 1 file changed, 609 insertions(+) create mode 100644 .agents/docs/2026-08-20-openkal-design.md diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md new file mode 100644 index 00000000..9a5adddb --- /dev/null +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -0,0 +1,609 @@ +# openkal 设计方案:通用内核 ABI 规范 + +**状态**:设计草案,**未实施**。对应第一阶段计划 §7 的 **D0/D1**,而 D0 的门不是技术判据 —— +是**「有第三方实现了第三个后端」**。本文的价值在于**不要在 D0 就把路堵死**。 + +**证据来源**:标注 ⓘ 的是本机实测(载荷版本写在旁边),其余是设计主张。 +调研出处见 [`2026-08-18-freestanding-baremetal-analysis.md`](2026-08-18-freestanding-baremetal-analysis.md)。 + +--- + +## 1. 定位:一份规范,两个方向 + +openkal **不是一个库,是一份内核 ABI 规范**。它有两类使用者,方向相反: + +``` + 应用 / libc / libc++ / Rust / ... + │ 对上:消费者「可以依赖什么」 + ════════════╪════════════ openkal SPEC + │ 对下:实现者「必须提供什么」 + Linux / Windows / 你写的内核 / 裸机 BSP +``` + +⭐ **关键性质:openkal 不知道自己在 libc 之上还是之下。** +hosted 后端把 `kal_stream_write` 转发到 `::write(2)`(在 libc 之上); +裸机后端直接打 MMIO(取代 libc);picolibc 后端把 `FILE::put` 接到它上面(在 libc 之下)。 +**三种都成立,而且是同一份契约。** + +ⓘ **实测(调研 KA1/KA3)**:同一份 `app.cppm`,零 `#if`,hosted 转 `::write(2)`、 +裸机转 MMIO UART;**两侧 `app.o` 的外部符号集完全相同**(都只有 `kal_write`); +裸机侧零未定义符号,157 字节。 + +### 1.1 这个性质由什么承载 + +**句柄的不透明性。** 一旦句柄有类型,openkal 就被钉在 libc 的某一侧: + +| 句柄形状 | 强迫的位置 | 后果 | +|---|---|---| +| `int fd` | libc 之下(要有 fd 表) | ⛔ Windows 后端要维护 fd→HANDLE 表 = **模拟层** | +| `FILE*` / stream 对象 | libc 之上 | ⛔ 裸机零 libc 档、Windows 都得造 FILE | +| **不透明一个机器字** | **不强迫** | ✅ 每个后端塞自己原生的东西 | + +--- + +## 2. 划分依据:资源种类,不是「点亮标准库」 + +⚠️ **一条被否掉的划分依据**:按「点亮标准库的哪一块」切接口。 +它耦合到 C++、耦合到今天的标准库,而且**依赖方向反了** —— 内核 ABI 应由内核提供什么定义。 + +而且那正是**同一个错误的第三代**:POSIX 由 C 的 stdio/unistd 塑形,WASIp1 由 POSIX 塑形。 + +⭐ **划分依据 = 交给你的是哪一种资源。** 语言无关,也是 WASIp2 / seL4 / Fuchsia 收敛到的形状。 + +「点亮标准库」退回它该在的位置:**D1 的准入判据**(见 §11.2),不是划分依据。 + +### 2.1 接口清单 + +| interface | 资源 | core? | 判据:无该设施的后端能实现吗 | +|---|---|---|---| +| **`openkal.abort`** | (终止) | ✅ | `for(;;) wfi` 是一个**实现** | +| **`openkal.stream`** | 一条字节流 | ✅ | null sink 是一个**实现** | +| **`openkal.memory`** | 一块内存区域 | ✅ | 静态 arena 上的 bump allocator 是一个**实现** | +| `openkal.time` | 一个时间源 | ❌ | ⚠️ 不走的计数器**不是**时钟 —— 会让超时静默失效 = **模拟** | +| `openkal.task` | 一个执行上下文 | ❌ | 需要调度 + 上下文切换(那是 openarch) | +| `openkal.namespace` | 名字 → 资源 | ❌ | 需要一个命名权威 | +| `openkal.channel` | 一条消息通道 | ❌ | — | + +### 2.2 ⭐ core 的判别式:实现 vs 模拟 + +> **假实现会让上层「静默地错」的 ⇒ 模拟;只是「容量小 / 会失败」的 ⇒ 实现。** + +`operator new` 失败在任何平台上都是**有定义的结果**,所以静态 arena 是实现。 +而一个不前进的时钟会让 `wait_for` 永远返回、熵不随机会让密钥可预测 —— 那是模拟。 + +⇒ **「这块 MCU 没有堆」是错的命题**:只要有 RAM,堆就是实现出来的; +上层不关心 openkal 底层怎么做到。 + +### 2.3 ⚠️ `fs` 与 `net` 不是接口 + +它们在资源分解下**溶解**了: + +``` +"tcp://10.0.0.1:80" ──namespace──▶ 一个 stream +"/etc/hosts" ──namespace──▶ 一个 stream +UART ──BSP 接线──▶ stdout(也是 stream) +``` + +⭐ **TCP 连接、文件、管道、串口给你的是同一种资源,只是命名方式不同。** +这比 POSIX 的 "everything is a file"(把命名和资源混成一个整数)干净: +**命名失败和读写失败是两件事,现在它们在两个接口里。** + +**net 不是设备,但网卡是** —— 按基数分(§10):网卡 N 个 → openhal;协议栈 1 个 → openkal。 + +--- + +## 3. 核心 ABI 形状 + +```c +/* openkal.core —— C ABI(H1:跨包提供实现只有这一条路)*/ + +typedef struct { uintptr_t h; } kal_stream; /* 不透明,1 个机器字 */ + +/* 标准流:借用,不拥有 —— 裸机上「关闭控制台」没有意义 */ +kal_stream kal_stdout(void); +kal_stream kal_stderr(void); +kal_stream kal_stdin (void); + +/* 2 字返回;T ≤ 1 个机器字 */ +typedef struct { uintptr_t n; int32_t err; } kal_io_result; + +kal_io_result kal_stream_write(kal_stream, const void* buf, uintptr_t len); +kal_io_result kal_stream_read (kal_stream, void* buf, uintptr_t len); +int32_t kal_stream_flush(kal_stream); + +/* memory */ +void* kal_alloc (uintptr_t size, uintptr_t align); +void kal_free (void* p, uintptr_t size, uintptr_t align); + +/* abort */ +_Noreturn void kal_abort(const char* msg, uintptr_t len); +_Noreturn void kal_exit (int32_t code); +``` + +四个后端往 `h` 里塞什么 —— **桥接代码全部为 0 行**: + +| 后端 | `h` | +|---|---| +| linux | `fd` | +| windows | `HANDLE` | +| bare + picolibc | ⓘ **`FILE*`(就是 `__stdio`)** | +| bare 零 libc | 驱动结构指针 / 小索引 | +| 真内核 | **能力索引** | + +### 3.1 为什么是这些形状 + +| 决定 | 理由 | +|---|---| +| **C ABI** | ⓘ H1 实测:跨包提供实现只有这一条路;且 KAL 一次 call 的成本**可忽略**(本来就要陷入) | +| **2 字结构返回** | ⓘ CABI 实测:两个 arch 上结构返回都更便宜;RISC-V 上就是 `a0/a1`,陷入桩天然能回。⚠️ **`T` 必须 ≤ 一个机器字**,否则退化成隐藏指针 | +| **不透明句柄** | §1.1;且真内核不能假设进程模型 / 全局命名空间,句柄必须是**调用方上下文相关**的 | +| **`enum class` 错误码,不透传 errno** | errno 是 POSIX 的。⚠️ 但**映射 ≠ 模拟**:查表翻译不是模拟层,造 fd 命名空间才是 | +| **自由函数,不要 vtable** | 虚表 = 把结构体布局写进 ABI,而「只增不改」保护不了它(加一个方法就改布局) | +| **没有 `open(path)`** | WASIp1 的教训:照抄 fd + 路径命名空间 ⇒ 每个非 POSIX 宿主都要模拟 preopen / `openat` | + +--- + +## 4. 能力组件化:三种机制,不能混用 + +⚠️ **先说一条会砍掉最直觉做法的实测**(mcpp 2026.8.19.4 上验的): + +```cpp +if constexpr (requires { kal::seek(s, 0); }) // ⚠️ 名字不存在 ⇒ 硬错误,不是 false +``` + +`requires` 作用在**不存在的限定名**上是 ill-formed。 +⇒ **「能力 = 符号在不在」这条路,C++ 语言内测不出来。** +**声明必须永远齐全,能力必须是另一个可命名的东西。** + +| | 例子 | 机制 | 失败时机 | +|---|---|---|---| +| **① 接口在不在** | 有没有 `openkal.task` | **模块导入** | 编译期,点名模块 | +| **② 接口内的操作在不在** | 有 `write` 没 `seek` | **constexpr caps** | 编译期,`static_assert` 文案 | +| **③ 语义能力** | 有没有 MMU、能否阻塞 | **`cfg()` 轴** | 依赖解析期 | + +⚠️ **③ 绝不能做成 concept**:ⓘ K1/K2 实测 `RiscvSv39` 与 `NoMmu` **同时满足**同一个 +`AddressSpace` concept,通用代码在 NoMmu 上**静默失败**。**concept 检查语法,不检查语义。** + +### 4.1 ①:让模块解析本身成为能力检查 + +``` +openkal.stream ← 接口:extern "C" 声明 + concepts + 类型化封装 +openkal.stream.caps ← 由「后端」提供 +``` + +```cpp +export module openkal.stream; +import openkal.stream.caps; // 没有后端 ⇒ 编译期找不到模块,点名它 +``` + +⇒ **「没有实现者」不是链接器吐未定义符号,而是编译器说模块不存在。** + +mcpp 侧承载件已有:后端由 **`cfg()` 条件依赖**选,接口包用 **`reexport = true`** +把后端 provisions 透给消费者 —— 对应「后端选择 = 条件依赖,零新增轴」。 + +### 4.2 ②:能力是**值** + +```cpp +export module openkal.stream.caps; +export namespace kal::stream_caps { +struct caps { + static constexpr bool sequential = true; + static constexpr bool seek = false; // 这个后端没有 + static constexpr bool vectored = true; + static constexpr bool nonblock = false; +}; +} +``` + +```cpp +if constexpr (kal::stream_caps::caps::seek) { kal::seek(s, off); } + +static_assert(kal::stream_caps::caps::seek, + "this backend has no seekable streams; openkal.namespace is the " + "interface that hands out seekable ones"); +``` + +组合 = **concept over caps**,不是 concept over 符号: + +```cpp +template concept Sequential = C::sequential; +template concept Seekable = Sequential && C::seek; +``` + +### 4.3 ③:`cfg()` 轴 —— ⚠️ 今天文法不够 + +mcpp 今天 `cfg()` 的文法是 **os / arch / family / env + all/any/not**,**没有能力谓词**。 +⇒ `cfg(mmu)` 这类需要**扩文法**。 + +⚠️ 这是本方案**唯一需要引擎改动**的一处,而且与「零新增轴」不冲突 —— +是给已有的 `cfg` 加一个词类,不是加一条新轴。 + +--- + +## 5. ⭐ caps 撒谎问题:四层防御,按强度排 + +**问题**:后端可以写 `caps::seek = true` 然后 `seek()` 永远失败。 +这是 K1/K2 的问题换了一层出现 —— caps 检查的是**有没有这个字段**,不是**它说的是不是真的**。 + +### 5.1 ① 让「不支持」在类型系统里**不可表达**(最强) + +如果一个操作可以「存在但永远失败」,**通常说明它被错误地合并了**。 + +ⓘ MMU 就是这个的实例:`NoMmu::map()` 对非恒等映射返回 `false` —— 那**不是撒谎, +是这个抽象本来就不该把两族东西装进一个 concept**。当时的结论是把它挪到 `cfg` 轴。 + +推广到 `seek`:socket 不能 seek,但那**不是后端撒谎,是名词错了**。 + +```cpp +// 不是:caps::seek = false +// 而是:句柄类型里根本没有那个能力位 +using console = kal::stream_of; // seek 不在 +using file = kal::stream_of; +kal::seek(c, 0); // ⚠️ 编译错误:重载要求 seek 位 +``` + +⇒ **能撒的谎少了一整类**,因为「`caps::x = true` 而 `x()` 无意义」在类型层面构造不出来。 + +### 5.2 ② caps 与实现**同源生成** + +不要让后端手写 caps。**一张表同时产出 caps 模块和源码选择**: + +``` +backend/capabilities.toml ← 唯一来源 + ├─→ mcpp:generated= → openkal.stream.caps + └─→ 源码选择 → seek.cpp 编不编进去 +``` + +⇒ **撒谎要改表,而改表就把实现一起删了。** +`mcpp:generated=` 与源码选择**都是今天已有的指令**,不需要新机制。 + +### 5.3 ③ 双向 conformance:`false` 也要验 + +> `caps::seek == false` 的后端,**必须不导出 seek 符号** —— `nm` 可查。 + +⭐ 价值在于**它不是行为测试,是对制品的静态检查**:又快又不可能漏测。 +两侧都钉,caps 才从「声称」变成「事实」。 + +### 5.4 ④ 过程兜底 + +conformance 结果进索引元数据:没过 seek 那组的后端,描述符里不允许声称。 +这是唯一能约束「实现者根本不跑 conformance」的东西。 + +### 5.5 ⚠️ 诚实的残余风险 + +**这四条都不证明行为。** 后端可以导出符号、通过 happy path、在某个输入上错。 +这是**每一份规范都有的残余风险**(POSIX 也一样),答案只能是 conformance 的覆盖度, +不存在语言层的解法。 + +⇒ **顺序很重要**:先靠 ① 让错误分类不可表达,再靠 ② 让撒谎自毁,③④ 只是兜底。 +一上来就写更严的 conformance,是在给一个**本可以消除的问题**加检查。 + +--- + +## 6. 实现者视角(对下) + +### 6.1 要交付什么 + +| | | +|---|---| +| 一组 `extern "C"` 定义 | §3 的清单,按你实现的 interface | +| 一个 `.caps` 模块 | ⚠️ **生成的**,不是手写的(§5.2) | +| conformance 通过记录 | 双向:声称有的能用,声称没有的**符号不存在** | + +### 6.2 最小实现:一个裸机 UART 后端 + +```cpp +// openkal-uart/src/stream.cpp +extern "C" { +kal_stream kal_stdout(void) { return kal_stream{1}; } // h = 一个小索引 +kal_stream kal_stderr(void) { return kal_stream{1}; } +kal_stream kal_stdin (void) { return kal_stream{0}; } + +kal_io_result kal_stream_write(kal_stream s, const void* buf, uintptr_t n) { + if (s.h != 1) return kal_io_result{0, KAL_EBADF}; + auto* p = static_cast(buf); + for (uintptr_t i = 0; i < n; ++i) + *reinterpret_cast(0x10000000) = p[i]; + return kal_io_result{n, 0}; +} +} +``` + +```toml +# capabilities.toml —— caps 与源码选择的唯一来源 +[stream] +sequential = true +seek = false # ⇒ seek.cpp 不编进去,符号也不存在 +vectored = false +``` + +### 6.3 后端怎么被选中 + +**条件依赖,零新增引擎轴**: + +```toml +[target.'cfg(all(arch = "riscv64", os = "none"))'.dependencies] +openkal-uart = "0.1" + +[target.'cfg(os = "linux")'.dependencies] +openkal-linux = "0.1" +``` + +### 6.4 ⚠️ 真内核实现者的额外约束 + +1. **不能假设进程模型 / 用户内核分界 / 全局命名空间** —— 单地址空间内核里没有「进程」 +2. **句柄必须是调用方上下文相关的**,不能是全局整数表下标 +3. **返回形状要能过陷入边界** —— 2 字结构在 RISC-V 上就是 `a0/a1` + +⇒ 三条**全部指向同一个结论**:不透明一个机器字的句柄。 + +--- + +## 7. 上层视角(对上) + +### 7.1 应用直接用 + +```cpp +import openkal.stream; + +int main() { + kal::write(kal::stdout(), "hello\n"); + if constexpr (kal::stream_caps::caps::vectored) { /* 用 writev 形状 */ } +} +``` + +ⓘ **KA2 实测:换后端不重编应用** —— 同一个 `app.o` 换掉后端目标文件重链即成。 + +### 7.2 类型化封装(C ABI 之上的零成本层) + +```cpp +export module openkal.stream; +import openkal.stream.caps; + +export namespace kal { + +enum class error : std::int32_t { ok = 0, badf, again, io, nospace, /*…*/ }; + +struct [[nodiscard]] io_result { + std::uintptr_t n; + error e; + constexpr explicit operator bool() const { return e == error::ok; } +}; + +inline io_result write(stream s, std::span b) { + auto r = kal_stream_write(s, b.data(), b.size()); + return { r.n, static_cast(r.err) }; +} + +// 便利重载:字符串 +inline io_result write(stream s, std::string_view sv) { + return write(s, std::as_bytes(std::span{sv.data(), sv.size()})); +} + +} +``` + +⚠️ `std::span` / `std::byte` / `std::string_view` **全都在 freestanding 可用的 103 个头里** +(ⓘ 实测,见 review §4)—— 这一层在裸机上是可以存在的。 + +--- + +## 8. 对接下层硬件:BSP 接线 + +⭐ **openhal 的设备实例可以成为 openkal 的后端** —— 这是接线,不是层级: + +```cpp +// BSP:把这块板的 openhal Serial 接成 openkal 的 stdout +import openhal.serial; + +namespace { + auto& uart = board::uart0(); // openhal 实例(concept,零成本) +} + +extern "C" kal_io_result kal_stream_write(kal_stream s, const void* buf, uintptr_t n) { + if (s.h != 1) return { 0, KAL_EBADF }; + uart.write({static_cast(buf), n}); // 内联进来 + return { n, 0 }; +} +``` + +⚠️ **注意成本**:openhal 是 concept,`uart.write` **可内联**;openkal 是 C ABI, +`kal_stream_write` 是一次真调用。这正是两者契约形态不同的原因(§10)。 + +--- + +## 9. 对接 libc:picolibc(⚠️ 缝是实测出来的) + +ⓘ **实测(picolibc 1.8.12,rv64gc/lp64d)** —— `libsemihost.a` 的 `common_iob.c.o`: + +``` +d __stdio ← FILE 实体 +R stdout / stdin / stderr ← 三个都指向它(同址) +U sys_semihost_putc ← 未定义 +U sys_semihost_getc ← 未定义 +``` + +而 **`libc.a` 根本不定义 `stdout`** —— picolibc 早就把「C 库」和「控制台在哪」分开了。 + +⇒ 移植到 openkal = **一个小 .o**: + +```cpp +// openkal-picolibc/src/console.cpp +#include +import openkal.stream; + +static int kal_putc(char c, FILE*) { + auto b = static_cast(c); + return kal::write(kal::stdout(), {&b, 1}) ? c : EOF; +} +static FILE __kal_stdio = FDEV_SETUP_STREAM(kal_putc, nullptr, nullptr, + _FDEV_SETUP_WRITE); +extern "C" FILE* const stdout = &__kal_stdio; +extern "C" FILE* const stderr = &__kal_stdio; +``` + +⭐ **成本论据**:`struct __file` 里 `put` **本来就是函数指针** +(`int (*put)(char, struct __file*)`)。接到 `kal` 上**不新增任何一层间接**。 + +⇒ **`printf` 和 `kal::write` 汇到同一个底,不产生第二条 I/O 路径。** + +⚠️ 反向也成立(openkal 在 libc **之上**):hosted 后端的 `kal_stream_write` 可以就是 +`fwrite(buf, 1, n, stdout)`。**同一份契约,两个方向。** + +--- + +## 10. 对接 libc++ / gcc 工具链 + +### 10.1 libc++:36 个名字,ⓘ **实测清单** + +ⓘ llvm 22.1.8 的 `__thread/support/` 有 **四个后端**:`pthread.h` `windows.h` `c11.h` +**`external.h`** —— 可插拔线程后端这个形状**在标准库里已经存在**。 + +`_LIBCPP_HAS_THREAD_API_EXTERNAL = 1` 后,要提供 `<__external_threading>`, +面是 ⓘ **36 个名字**(从 `pthread.h` 数出来的,与调研一致): + +``` +mutex×4 __libcpp_mutex_t / _lock / _trylock / _unlock / _destroy +recursive_mutex×5 __libcpp_recursive_mutex_t / _init / _lock / _trylock / _unlock / _destroy +condvar×5 __libcpp_condvar_t / _signal / _broadcast / _wait / _timedwait / _destroy +thread×9 __libcpp_thread_t / _id / _create / _join / _detach / _yield / _sleep_for … +once×1 __libcpp_execute_once (+ __libcpp_exec_once_flag) +tls×3 __libcpp_tls_key / _create / _get / _set +``` + +映射到 `openkal.task` 应当是**一一对应**,不需要适配层: + +```cpp +// openkal 侧 <__external_threading> 的实现骨架 +using __libcpp_mutex_t = kal_mutex; // 不透明,1 字 +inline int __libcpp_mutex_lock(__libcpp_mutex_t* m) { + return static_cast(kal_mutex_lock(*m)); // 直调,零桥接 +} +``` + +⚠️ **这听起来像「又在照抄」,区别是实质的**:`__external_threading` 本来就是给 +**非 POSIX 系统**用的插点,它已经过了「非 POSIX 宿主能不能实现」这道筛;POSIX 没有。 +**照抄一个可移植性插点 ≠ 照抄一个 OS。** + +### 10.2 gcc / libstdc++:gthreads + +ⓘ **实测(gcc 16.1.0)**:`bits/gthr-default.h` 里 `__gthread_*` 共 **71** 个名字, +其中 **20** 个是 `__gthread_objc_*` 遗留 ⇒ **真实面 51**,与 libc++ 的 36 高度重合。 + +⭐ ⓘ 更重要的先例:gcc 自带 **`gthr-single.h`** —— **「没有线程」是一个编译期后端选择**, +不是运行期 `ENOSYS`。**这正是本方案 §4 主张的形状,而且 gcc 已经这么做了三十年。** + +⇒ openkal 的 gcc 侧对接 = 提供一份 `gthr-openkal.h`,和 `gthr-posix.h` 平级。 + +### 10.3 `operator new` 与 `__libcpp_verbose_abort` + +这两个是**标准扩展点**,不需要发明契约: + +```cpp +void* operator new(std::size_t n) { + if (void* p = kal_alloc(n, __STDCPP_DEFAULT_NEW_ALIGNMENT__)) return p; + kal_abort("operator new failed", 20); +} +_LIBCPP_BEGIN_NAMESPACE_STD +[[noreturn]] void __libcpp_verbose_abort(const char* f, ...) _NOEXCEPT { + kal_abort(f, __builtin_strlen(f)); +} +_LIBCPP_END_NAMESPACE_STD +``` + +ⓘ ⚠️ **`__libcpp_verbose_abort` 必须通过 libc++ 自己的头声明** —— 真符号在 ABI 内联 +命名空间 `std::__1::` 里,手写 `namespace std { }` **编得过、链不上、报错一字不变** +(本轮实测踩过)。 + +### 10.4 ⚠️ openkal 明确**不认领** `thread_local` + +ⓘ 实测的静默失败: + +``` +thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零诊断 ✅ + → 运行期通过未设置的 tp 读写垃圾地址 +``` + +`thread_local` **不属于线程契约**,属于 **openarch(TLS 寄存器约定)+ BSP +(`start.S` 设 `tp` + 链接脚本 `.tdata/.tbss`)**。 +而 `__external_threading` 里的 `tls×3` 是 **pthread_key 那种动态 TLS**,不是它。 + +⇒ **规范里必须写死这条边**,否则「我实现了 openkal 的线程」会被读成「`thread_local` 能用了」。 +⚠️ 且 libc++/libstdc++ **自己内部就用 `thread_local`**,不是「用户不写就没事」。 + +--- + +## 11. C++ 特性 → 解决什么问题 + +| 特性 | 解决什么 | ⚠️ 限制 | +|---|---|---| +| **modules** | 接口/caps 分离;**模块找不到 = 能力检查**,把链接期错误提前到编译期 | 需要 mcpp 的 `reexport`/provisions 承载 | +| **`constexpr` caps 描述符** | 能力是**值**不是符号 —— 绕开「`requires` 测不了缺失名字」 | — | +| **`if constexpr`** | 消费者按能力降级,未选中的分支**不实例化** | — | +| **concepts** | 组合 caps;openhal 的多提供者共存 | ⚠️ ⓘ **只检查语法不检查语义**(K1/K2),③ 类能力不能用它 | +| **非类型模板参数(能力位)** | 让「不支持」**在类型系统里不可表达**(§5.1) | — | +| **`static_assert` + 文案** | 把缺能力变成**人能读**的编译错,而不是链接器的符号名 | — | +| **`enum class`** | 封闭错误集合,不透传 errno | — | +| **`[[nodiscard]]`** | `io_result` 不能被静默丢弃 | — | +| **`std::span` / `std::byte` / `string_view`** | C ABI 之上的类型化层 | ⓘ 三者都在 freestanding 可用的 103 头里 | +| **`_Noreturn` / `[[noreturn]]`** | `kal_abort` 的控制流事实进类型 | — | +| ~~virtual / vtable~~ | — | ⛔ 把结构体布局写进 ABI,「只增不改」保护不了 | +| ~~exceptions / RTTI~~ | — | ⛔ 裸机整图关闭(mcpp 2026.8.19.4 起) | +| ~~`requires` 探测符号~~ | — | ⛔ ⓘ **实测:硬错误,不是 `false`** | + +--- + +## 12. 与 openhal / openarch 的关系:**契约形态由基数决定** + +| | 一个程序里几个实现 | 契约 | 为什么 | +|---|---|---|---| +| **openarch** | **1**(一颗 CPU) | concept **+ LTO 必需** | ⓘ K3:`local_irq_disable()` 本该一条 `csrci`;跨模块 `-O2` 退化成真 `jal`,`-flto` 才恢复 | +| **openkal** | **1**(一个内核) | **C ABI** | ⓘ 一次 call 可忽略(本来就陷入);H1 | +| **openhal** | **N**(每条总线/设备) | **concept** | ⓘ H3:多提供者共存 + 零成本可内联 | + +⭐ **openarch 与 openhal 都是 concept,但理由相反**:一个因为调用成本灾难性, +一个因为要多实现共存。openkal 两种压力都没有 ⇒ 用最简单也最稳定的 C ABI。 + +**openkal.stream 与 openhal.Console 的边界**: + +- openkal 的流 = **程序的标准流**:一个、环境给的、借用的 ——「我的诊断往哪去」 +- openhal 的 `Console`/`Serial` = **你打开的一个设备**:多个、拥有的、有类型 + +**是两个名词,不是一个东西的两层。** 自然组合见 §8。 + +--- + +## 13. 判据与门 + +### 13.1 双向准入判据(⚠️ 文档今天只有对下那一半) + +| 方向 | 判据 | 怎么数 | +|---|---|---| +| **对下** | 四个后端自然实现,**没有模拟层** | 写出四个后端的 `kal_stream_write`,数「只为迁就形状而存在」的行数 —— 任何后端需要**表/注册中心/路径解析器**,形状就错了 | +| **对上** | libc / libc++ / gcc 的**既有插点**直接落上,**没有适配层** | `stdout->put` · `operator new` · `__libcpp_verbose_abort` · 36 个 `__libcpp_*` · `gthr-*.h` —— 同样数桥接行数 | + +⇒ **对上判据不满足 ⇒ 说明资源分解切错了,回去改分解** —— 而不是改边界迁就标准库。 + +### 13.2 D0–D3 的门(来自第一阶段计划 §7,不改) + +| 阶段 | ⭐ 继续的判据 | ⚠️ 停止的信号 | +|---|---|---| +| **D0** | **有第三方实现了第三个后端** —— 这才是 D 能否成立的真变量 | 半年内无人实现 ⇒ 停在 D0,当 mcpp 内部设施 | +| **D1** | 三个官方后端全过 conformance;⚠️ windows 后端**不经 MSVC CRT 的 POSIX 兼容层** | windows 做不出来而不模拟 ⇒ **SPEC 形状错了**,回炉 | +| **D2**(openhal) | 同一个驱动包既跑裸机 MCU 又跑 Linux | 驱动作者不来 ⇒ 停;不影响 openkal | +| **D3**(openarch) | ⚠️ 两个最硬原语不碎(上下文切换 / 页表项) | 碎掉 ⇒ **停**;⚠️ 碎在这里后面全是幻觉 | + +--- + +## 14. 明确不做的 + +| | 为什么 | +|---|---| +| **`open(path)` 进 core** | WASIp1 的教训;命名进 `openkal.namespace`,且不是 core | +| **运行期 `ENOSYS`** | 部分性用「链接哪些 interface」表达;⭐ gcc 的 `gthr-single.h` 已是三十年的先例 | +| **errno 透传** | 封闭 `enum class`;映射不是模拟 | +| **虚接口 / vtable** | ABI 脆弱,与「只增不改」冲突 | +| **`thread_local` 的保证** | 属于 openarch + BSP(§10.4) | +| **引擎认识 openkal** | ⚠️ 计划 §7.2 第一行:**引擎永不认识这三层**;后端选择 = 条件依赖,零新增轴 | +| **现在就实现** | D0 的门是「第三方来了没有」,不是「能不能写出来」 | From 910ec2bf594564b271b75ce06aef40aba0dd36b2 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:15:03 +0800 Subject: [PATCH 04/10] docs(openkal): retract two decisions after review, and record six open questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/docs/2026-08-20-openkal-design.md | 217 +++++++++++++++++++--- 1 file changed, 195 insertions(+), 22 deletions(-) diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md index 9a5adddb..b700a93e 100644 --- a/.agents/docs/2026-08-20-openkal-design.md +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -61,9 +61,14 @@ hosted 后端把 `kal_stream_write` 转发到 `::write(2)`(在 libc 之上); | **`openkal.memory`** | 一块内存区域 | ✅ | 静态 arena 上的 bump allocator 是一个**实现** | | `openkal.time` | 一个时间源 | ❌ | ⚠️ 不走的计数器**不是**时钟 —— 会让超时静默失效 = **模拟** | | `openkal.task` | 一个执行上下文 | ❌ | 需要调度 + 上下文切换(那是 openarch) | -| `openkal.namespace` | 名字 → 资源 | ❌ | 需要一个命名权威 | +| `openkal.fs` | 一个 **descriptor**(自有句柄类型) | ❌ | 需要一个命名权威 | +| `openkal.net` | 一个 **socket**(自有句柄类型) | ❌ | 同上 | | `openkal.channel` | 一条消息通道 | ❌ | — | +⭐ **`stream` 是共享货币,不是统一入口**:`fs` 的 descriptor 与 `net` 的 socket 各有 +自己的句柄类型和自己的操作,但**都能产出 `openkal.stream`**。往哪写这件事对文件 / +socket / UART 是同一套代码;打开它们不是。 + ### 2.2 ⭐ core 的判别式:实现 vs 模拟 > **假实现会让上层「静默地错」的 ⇒ 模拟;只是「容量小 / 会失败」的 ⇒ 实现。** @@ -74,21 +79,48 @@ hosted 后端把 `kal_stream_write` 转发到 `::write(2)`(在 libc 之上); ⇒ **「这块 MCU 没有堆」是错的命题**:只要有 RAM,堆就是实现出来的; 上层不关心 openkal 底层怎么做到。 -### 2.3 ⚠️ `fs` 与 `net` 不是接口 +### 2.3 ⚠️ 一个被起草后撤回的分解:`openkal.namespace` + +草案曾把 `fs` 和 `net` 消掉,换成一个通用的 `openkal.namespace`(名字 → 资源), +理由是「文件 / TCP 连接 / 管道 / 串口给你的都是 stream,只是命名方式不同」。 + +**重估后撤回。三条攻击全部成立:** + +**① 它触犯本文自己的 §5.1 规矩。** 「一个操作若能『存在但永远失败』,说明它被错误地 +合并了」—— 而把文件和 socket 都塞进一个 `stream`,`stream` 的 caps 就成了 +**互不相干能力的并集**(seek/size/truncate/sync 对上 shutdown/peer/nodelay), +每个后端对其中大多数说 `false`。**正是那个反模式,换了个地方出现。** + +**② `namespace` 需要一个 URI 解析器,那是模拟层。** `kal_namespace_open("tcp://…")` +要求**每个后端都能解析 scheme**:只有 UART 的后端也得解析并拒绝 `tcp://`; +合法 scheme 集合无界、不可发现;错误是字符串形状的。 +⚠️ **直接违反对下判据**(四后端自然实现、不需模拟层),而且比 POSIX 还差 —— +POSIX 至少 `open()` 与 `socket()+connect()` 是分开的类型化调用。 -它们在资源分解下**溶解**了: +**③ 引用的先例是错的。** 草案称「这是 WASIp2 收敛到的形状」。**不是。** WASIp2 是: ``` -"tcp://10.0.0.1:80" ──namespace──▶ 一个 stream -"/etc/hosts" ──namespace──▶ 一个 stream -UART ──BSP 接线──▶ stdout(也是 stream) +wasi:io/streams input-stream / output-stream ← 共享的传输资源 +wasi:filesystem/types descriptor ← 自有资源类型,能产出 stream +wasi:sockets/tcp tcp-socket ← 自有资源类型,能产出 stream ``` -⭐ **TCP 连接、文件、管道、串口给你的是同一种资源,只是命名方式不同。** -这比 POSIX 的 "everything is a file"(把命名和资源混成一个整数)干净: -**命名失败和读写失败是两件事,现在它们在两个接口里。** +它把**资源种类分开**,共享的是 **stream 这个传输类型**。草案把「共享 stream」 +误读成了「统一命名」。 + +⇒ **保留对的那半(流统一了传输),丢掉错的那半(统一命名)。** + +| | namespace 草案 | 撤回后 | 单体 fs+net | +|---|---|---|---| +| 实现者 | ⛔ 人人要 URI 解析器 | ✅ 没有就**不提供**该 interface | ✅ 同 | +| 消费者 | ⛔ 错误是字符串;**编译期不知道支不支持** | ⭐ `import openkal.net;` 缺了就**编译期报错** | ✅ 同 | +| 规范负担 | ⛔ 要标准化 **scheme 注册表** = 巨大隐藏面 | ✅ 每 interface 独立版本,面有界 | ⚠️ 接口大但有界 | +| 类型安全 | ⛔ caps 成为不相干能力并集 | ✅ 文件操作在文件句柄上 | ✅ 同 | + +⚠️ **划分原则(按资源种类)没错,错的是塌缩过头** —— `descriptor` / `socket` / +`stream` 本来就是三种资源。 -**net 不是设备,但网卡是** —— 按基数分(§10):网卡 N 个 → openhal;协议栈 1 个 → openkal。 +**net 不是设备,但网卡是** —— 按基数分(§12):网卡 N 个 → openhal;协议栈 1 个 → openkal。 --- @@ -159,11 +191,14 @@ if constexpr (requires { kal::seek(s, 0); }) // ⚠️ 名字不存在 ⇒ 硬 |---|---|---|---| | **① 接口在不在** | 有没有 `openkal.task` | **模块导入** | 编译期,点名模块 | | **② 接口内的操作在不在** | 有 `write` 没 `seek` | **constexpr caps** | 编译期,`static_assert` 文案 | -| **③ 语义能力** | 有没有 MMU、能否阻塞 | **`cfg()` 轴** | 依赖解析期 | +| **③ 语义能力** | 抢占式 vs 协作式调度 | **`cfg()` 轴** | 依赖解析期 | ⚠️ **③ 绝不能做成 concept**:ⓘ K1/K2 实测 `RiscvSv39` 与 `NoMmu` **同时满足**同一个 `AddressSpace` concept,通用代码在 NoMmu 上**静默失败**。**concept 检查语法,不检查语义。** +⭐ **但 openkal core 里一条 ③ 都没有** —— 见 §4.3。K1/K2 那个例子是 **openarch** 的 +`AddressSpace`,不是 openkal 的。 + ### 4.1 ①:让模块解析本身成为能力检查 ``` @@ -178,8 +213,31 @@ import openkal.stream.caps; // 没有后端 ⇒ 编译期找不到模块,点 ⇒ **「没有实现者」不是链接器吐未定义符号,而是编译器说模块不存在。** -mcpp 侧承载件已有:后端由 **`cfg()` 条件依赖**选,接口包用 **`reexport = true`** -把后端 provisions 透给消费者 —— 对应「后端选择 = 条件依赖,零新增轴」。 +⚠️ **接法要注意方向。** 草案曾写「接口包 `reexport` 后端的 provisions」—— +**错的**:ⓘ `reexport` 是**向下游**传播(`grpc` 把 protoc 透给它的用户), +而这里需要的是接口拿到**消费者所选后端**提供的东西,方向相反,`reexport` 表达不了。 + +正确接法是**反过来**,而且正好是 `reexport` 的本意: + +```toml +# 后端包 openkal-uart 的 manifest +[dependencies] +openkal-stream = { version = "0.1", reexport = true } # 把接口透给我的消费者 +``` + +```toml +# 消费者:只写后端,按 target 选;接口随之而来 +[target.'cfg(os = "linux")'.dependencies] +openkal-linux = "0.1" +[target.'cfg(os = "none")'.dependencies] +openkal-uart = "0.1" +``` + +源码 `import openkal.stream;` 两个 target 一字不改。 +⇒ **零新增引擎轴,且用的是已有机制的本意。** + +⚠️ 两个后端同时进图会造成 `openkal.stream.caps` 模块重复定义 —— 基数为 1(§12) +使这成为用户错误,mcpp 会报模块冲突。 ### 4.2 ②:能力是**值** @@ -199,8 +257,8 @@ struct caps { if constexpr (kal::stream_caps::caps::seek) { kal::seek(s, off); } static_assert(kal::stream_caps::caps::seek, - "this backend has no seekable streams; openkal.namespace is the " - "interface that hands out seekable ones"); + "this backend has no seekable streams; openkal.fs hands out " + "descriptors that do"); ``` 组合 = **concept over caps**,不是 concept over 符号: @@ -210,15 +268,27 @@ template concept Sequential = C::sequential; template concept Seekable = Sequential && C::seek; ``` -### 4.3 ③:`cfg()` 轴 —— ⚠️ 今天文法不够 +### 4.3 ③:⚠️ openkal core 用不到它 —— 一条被撤回的引擎改动 -mcpp 今天 `cfg()` 的文法是 **os / arch / family / env + all/any/not**,**没有能力谓词**。 -⇒ `cfg(mmu)` 这类需要**扩文法**。 +草案曾要求扩 `cfg()` 文法以支持 `cfg(mmu)` 这类能力谓词。**重估后撤回。** -⚠️ 这是本方案**唯一需要引擎改动**的一处,而且与「零新增轴」不冲突 —— -是给已有的 `cfg` 加一个词类,不是加一条新轴。 +那条结论的出处是 K1/K2,而 K1/K2 测的是 **openarch 的 `AddressSpace`** —— 草案把它 +搬进了 openkal。逐个接口检查 openkal 有没有「存在但语义不同」的能力: ---- +| interface | 有 ③ 类语义轴吗 | +|---|---| +| `abort` | 无 | +| `stream` | seek / nonblock / vectored 都是**操作**(②类) | +| `memory` | 静态 arena vs 按需分页 = **容量**不是能力;分配失败到处都有定义 | +| `time` | monotonic vs wall 是**两种资源**,不是一个资源的两种语义 | +| `task` | ⚠️ 抢占 vs 协作**确实是** —— 但那是 D1 以后的事 | + +⇒ **core(abort + stream + memory)一条语义轴都不需要,①② 足够。** + +而且 **triple 本身已经承载了大部分**:`riscv64-none-elf` 与 `riscv64-linux-gnu` 的 +区别里就包含了 MMU 用不用。今天已有的文法足以选后端。 + +⭐ **撤回后,本方案变成零引擎改动。** ## 5. ⭐ caps 撒谎问题:四层防御,按强度排 @@ -600,10 +670,113 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零 | | 为什么 | |---|---| -| **`open(path)` 进 core** | WASIp1 的教训;命名进 `openkal.namespace`,且不是 core | +| **`open(path)` 进 core** | WASIp1 的教训;开文件在 `openkal.fs`(自有句柄类型),不在 core,也不经一个通用命名接口(§2.3) | | **运行期 `ENOSYS`** | 部分性用「链接哪些 interface」表达;⭐ gcc 的 `gthr-single.h` 已是三十年的先例 | | **errno 透传** | 封闭 `enum class`;映射不是模拟 | | **虚接口 / vtable** | ABI 脆弱,与「只增不改」冲突 | | **`thread_local` 的保证** | 属于 openarch + BSP(§10.4) | | **引擎认识 openkal** | ⚠️ 计划 §7.2 第一行:**引擎永不认识这三层**;后端选择 = 条件依赖,零新增轴 | | **现在就实现** | D0 的门是「第三方来了没有」,不是「能不能写出来」 | + +--- + +## 15. 撤回记录 + +⚠️ 草案里被 review 推翻的两条,连同它们**为什么当时看起来对**: + +| 撤回的 | 当时的理由 | 为什么错 | +|---|---|---| +| **`openkal.namespace`**(取代 fs/net) | 「文件 / socket / UART 给你的都是 stream,只是命名方式不同」 | ① 触犯本文自己的 §5.1 规矩(caps 成为不相干能力并集);② URI 解析器**就是**模拟层,违反对下判据;③ 引用的 WASIp2 先例是**误读**(它分开资源种类,只共享 stream 类型) | +| **扩 `cfg()` 文法支持 `cfg(mmu)`** | K1/K2 说「MMU 是能力轴不是契约」 | 那条结论是关于 **openarch** 的;openkal core 逐个接口查下来**一条 ③ 类语义轴都没有**,而且 triple 本身已承载大部分 | +| **「接口包 `reexport` 后端」** | 以为 mcpp 的承载件现成 | ⓘ `reexport` 是**向下游**传播,方向相反。正确接法是**后端 reexport 接口**(§4.1),恰好是该机制的本意 | + +⭐ 三条的共同形状:**都是「我有一个漂亮的统一」,而漂亮的统一把两件本来不同的事合并了。** +这与 §5.1 给出的判据是同一条 —— 只是那一节我用它去检查别人的后端,没有用它检查自己的分解。 + +--- + +## 16. 综合 review 发现的开放问题 + +撤回两条之后重新通读,又找出六条 —— 都是**规范必须表态、但草案没表态**的。 + +### 16.1 ⚠️ 两个堆(ⓘ 实测,不是理论) + +ⓘ picolibc `libc.a` 的 `vfprintf.c.o` **引用 `free`** —— **printf 与分配器是耦合的**。 +而固件里已有一整套 `malloc`/`free`/`__malloc_sbrk_aligned`/`__fallback_sbrk`。 + +⇒ 若 `operator new` 走 `kal_alloc`(openkal 的 arena)而 `printf` 走 picolibc 的 +`malloc`,**同一块 RAM 上会有两个分配器**,而且都想长 sbrk。 + +**规范必须写死一条**: + +> **后端上若已存在 libc 分配器,`kal_alloc` 必须实现在它之上,而不是与它并列。** + +三种配置: + +| 后端 | 做法 | | +|---|---|---| +| picolibc | `kal_alloc` → `malloc`(openkal 在 libc **之上**) | ✅ 一个堆 | +| 零 libc | `kal_alloc` → 自带 arena;没有 libc malloc | ✅ 一个堆 | +| ⛔ 自带 arena **且** libc malloc 也在 | — | **禁止** | + +⚠️ 这条部分可 conformance 化:检查固件里 `sbrk` 的消费者是不是只有一个。 + +### 16.2 ⚠️ 错误集合的封闭性 + +草案说「封闭 `enum class`,不透传 errno」。但 POSIX 有约 130 个 errno, +一个 ~15 项的封闭集合**必然丢信息**。规范要表态: + +- 丢掉细节(简单,但诊断质量下降),还是 +- 留一个 `other` + **后端私有的细节通道**(`kal_last_error_detail()`), + ⚠️ 但那是全局状态,和 errno 一样的毛病 + +**倾向**:封闭集合 + 细节通道**只用于日志**,不进控制流。需要写进 SPEC。 + +### 16.3 ⚠️ C ABI 没有版本,而结构体布局会被永久冻结 + +「每个 interface 独立版本 + 只增不改」保护得了**函数**(加新函数是可加的), +保护不了**结构体**:`kal_io_result` 的布局一旦发布就永远不能动。 + +选项:符号带版本后缀(`kal_stream_write_v1`,难看但诚实),或**明确声明这些布局永久冻结**。 +草案默认了后者却没写出来。 + +### 16.4 ⚠️ 拥有 vs 借用,C ABI 强制不了 + +标准流是**借用**(不配 `close`),`openkal.fs` 的 descriptor 是**拥有**(必须 close)。 +C ABI 里没有 RAII,谁来保证? + +⇒ 类型化 C++ 层可以包 RAII,但**那不是规范的一部分**,Rust/C 消费者拿不到。 +规范至少要把「哪些句柄是拥有的」写清楚,并规定重复 close 的行为。 + +### 16.5 ⚠️ 线程安全未规定 + +同一个 `kal_stream` 被两个 task 同时 `write`,是什么行为? +POSIX 至少规定了 `PIPE_BUF` 以内的原子性。**草案一个字没提。** +这条在有 `openkal.task` 之后立刻变成必须回答的。 + +### 16.6 ⚠️ sized-free 的方向性成本 + +`kal_free(p, size, align)` 对 arena 友好(Rust 的做法),但: + +- hosted 后端 `kal_free` → `free(p)`,**丢掉 size**:无成本 ✅ +- 反向:一个建在 `kal_alloc` 之上的 libc `malloc` **必须自己存 size** ⇒ 每次分配多一个字 + +⚠️ 与 §16.1 的规则合看,反向配置本来就该避免,所以成本可控 —— 但要写明。 + +--- + +## 17. 这一轮 review 的元结论 + +三条撤回(§15)+ 六条开放问题(§16)里,有一个共同形状值得单独记: + +⭐ **草案的错误全部是「一个漂亮的统一,把两件本来不同的事合并了」**: + +- `openkal.namespace` 合并了「命名」与「资源种类」 +- `cfg(mmu)` 把 openarch 的结论搬进 openkal +- 「两个堆」是没有合并该合并的(分配器) + +而 §5.1 给出的判据 —— **「一个操作若能『存在但永远失败』,说明它被错误地合并了」** —— +本来就能抓住前两条。 + +⚠️ **我只用它去检查别人的后端,没有用它检查自己的分解。** +⇒ 判据要对**自己的设计**先跑一遍,再拿去当准入门槛。 From e25429c1df62c6747addb12a0ad0c5ff13abacc3 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:27:18 +0800 Subject: [PATCH 05/10] =?UTF-8?q?docs(openkal):=20capabilities=20are=20ADL?= =?UTF-8?q?-probeable=20=E2=80=94=20the=20caps=20struct=20and=20its=20conf?= =?UTF-8?q?ig=20file=20are=20gone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/docs/2026-08-20-openkal-design.md | 262 +++++++++++----------- 1 file changed, 131 insertions(+), 131 deletions(-) diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md index b700a93e..60873b16 100644 --- a/.agents/docs/2026-08-20-openkal-design.md +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -175,52 +175,90 @@ _Noreturn void kal_exit (int32_t code); --- -## 4. 能力组件化:三种机制,不能混用 +## 4. 能力组件化:ADL 探测 + 兜底重载 -⚠️ **先说一条会砍掉最直觉做法的实测**(mcpp 2026.8.19.4 上验的): +⚠️ **草案在这里错了一整节。** 它说: + +> `requires` 作用在不存在的限定名上是硬错误 ⇒ 「能力 = 符号在不在」这条路 C++ 内测不出来 +> ⇒ 声明必须永远齐全,能力要放进一个 `caps` 结构体。 + +**第一句对,后面全错。** 硬错误只发生在**限定名**上;**非限定名 + ADL** 在模板里是 +dependent 的,`requires` 会老实求值为 `false`。 + +ⓘ **实测(llvm 22.1.8,真 C++20 模块,不是头文件)**: ```cpp -if constexpr (requires { kal::seek(s, 0); }) // ⚠️ 名字不存在 ⇒ 硬错误,不是 false +// ⛔ 限定名:名字不存在 ⇒ 硬错误 +if constexpr (requires(S s) { kal::seek(s, 0); }) + +// ✅ 非限定 + ADL:名字不存在 ⇒ false +template concept Seekable = requires(S s, long o) { seek(s, o); }; ``` -`requires` 作用在**不存在的限定名**上是 ill-formed。 -⇒ **「能力 = 符号在不在」这条路,C++ 语言内测不出来。** -**声明必须永远齐全,能力必须是另一个可命名的东西。** +⇒ **能力不需要另一个可命名的东西。后端的模块接口本身就是能力声明。** -| | 例子 | 机制 | 失败时机 | -|---|---|---|---| -| **① 接口在不在** | 有没有 `openkal.task` | **模块导入** | 编译期,点名模块 | -| **② 接口内的操作在不在** | 有 `write` 没 `seek` | **constexpr caps** | 编译期,`static_assert` 文案 | -| **③ 语义能力** | 抢占式 vs 协作式调度 | **`cfg()` 轴** | 依赖解析期 | +### 4.1 三件事同时成立(ⓘ 全部实测) -⚠️ **③ 绝不能做成 concept**:ⓘ K1/K2 实测 `RiscvSv39` 与 `NoMmu` **同时满足**同一个 -`AddressSpace` concept,通用代码在 NoMmu 上**静默失败**。**concept 检查语法,不检查语义。** +```cpp +export module openkal.stream; +export namespace kal { -⭐ **但 openkal core 里一条 ③ 都没有** —— 见 §4.3。K1/K2 那个例子是 **openarch** 的 -`AddressSpace`,不是 openkal 的。 +struct stream { unsigned long h; }; +struct io_result { unsigned long n; int e; }; -### 4.1 ①:让模块解析本身成为能力检查 +// ⭐ 兜底重载的返回类型与真实现不同 —— 这是让「探测」与「兜底」共存的关键 +struct unsupported_t {}; +template inline constexpr bool always_false = false; +template +unsupported_t seek(S, long) { + static_assert(always_false, + "this openkal backend provides no seekable streams. " + "openkal.fs hands out descriptors that do."); + return {}; +} + +// 探测:要求返回真类型,兜底自动落选 +template concept HasSeek = + requires(S s, long o) { requires __is_same(decltype(seek(s, o)), io_result); }; + +} ``` -openkal.stream ← 接口:extern "C" 声明 + concepts + 类型化封装 -openkal.stream.caps ← 由「后端」提供 -``` + +后端只需要**声明并定义真实现**,不需要任何配置: ```cpp -export module openkal.stream; -import openkal.stream.caps; // 没有后端 ⇒ 编译期找不到模块,点名它 +export module openkal.uart; +export import openkal.stream; +export namespace kal { io_result seek(stream, long) { /* … */ } } ``` -⇒ **「没有实现者」不是链接器吐未定义符号,而是编译器说模块不存在。** +| 场景 | 结果 | ⓘ | +|---|---|---| +| 有后端,调用 `seek(s, 0)` | ✅ 编过,真实现赢重载 | 实测 | +| 有后端,`HasSeek` | ✅ **真** | 实测 | +| 无后端,`HasSeek` | ✅ **假**(不是硬错误)⇒ `if constexpr` 可优雅降级 | 实测 | +| 无后端,**强行调用** | ⭐ **编译期报错,文案是规范自己写的那句** | 实测 | + +⚠️ **这里有一个必须踩过才知道的坑**:如果兜底重载的返回类型**和真实现一样**, +`HasSeek` 在没有后端时也是**真** —— 因为 ADL 找到了兜底,而 `requires` 不实例化函数体, +`static_assert` 不会触发。**两个机制互相干扰,靠返回类型区分才能共存。** +ⓘ 我第一版就是这么写的,测出来是真才发现。 -⚠️ **接法要注意方向。** 草案曾写「接口包 `reexport` 后端的 provisions」—— -**错的**:ⓘ `reexport` 是**向下游**传播(`grpc` 把 protoc 透给它的用户), -而这里需要的是接口拿到**消费者所选后端**提供的东西,方向相反,`reexport` 表达不了。 +### 4.2 ⇒ 草案里三样东西被删掉了 + +| 删掉的 | 为什么不再需要 | +|---|---| +| `caps` 结构体 | 后端的模块接口就是声明 | +| `openkal.stream.caps` 模块 | 同上 | +| `capabilities.toml` | ⭐ **生态整洁性**:mcpp 的一切都在 `mcpp.toml` 里,不该为这个引入第二份配置 | -正确接法是**反过来**,而且正好是 `reexport` 的本意: +**后端选择仍然只是条件依赖**(§4.3),整条链路**零新增配置、零新增引擎轴**。 + +### 4.3 后端怎么被选中:只用 `mcpp.toml` ```toml -# 后端包 openkal-uart 的 manifest +# 后端包 openkal-uart [dependencies] openkal-stream = { version = "0.1", reexport = true } # 把接口透给我的消费者 ``` @@ -234,120 +272,61 @@ openkal-uart = "0.1" ``` 源码 `import openkal.stream;` 两个 target 一字不改。 -⇒ **零新增引擎轴,且用的是已有机制的本意。** - -⚠️ 两个后端同时进图会造成 `openkal.stream.caps` 模块重复定义 —— 基数为 1(§12) -使这成为用户错误,mcpp 会报模块冲突。 - -### 4.2 ②:能力是**值** -```cpp -export module openkal.stream.caps; -export namespace kal::stream_caps { -struct caps { - static constexpr bool sequential = true; - static constexpr bool seek = false; // 这个后端没有 - static constexpr bool vectored = true; - static constexpr bool nonblock = false; -}; -} -``` +⚠️ ⓘ 方向要注意:`reexport` 是**向下游**传播(`grpc` 把 protoc 透给用户), +所以是**后端 reexport 接口**,不是接口 reexport 后端 —— 草案写反过。 -```cpp -if constexpr (kal::stream_caps::caps::seek) { kal::seek(s, off); } - -static_assert(kal::stream_caps::caps::seek, - "this backend has no seekable streams; openkal.fs hands out " - "descriptors that do"); -``` +### 4.4 接口层面的缺失(①) -组合 = **concept over caps**,不是 concept over 符号: +一整个 interface 不存在时,`import openkal.task;` **编译期就找不到模块**。 +这一条不变,而且和 §4.1 是同一套失败语义:**编译期,点名,不是运行期返回值。** -```cpp -template concept Sequential = C::sequential; -template concept Seekable = Sequential && C::seek; -``` - -### 4.3 ③:⚠️ openkal core 用不到它 —— 一条被撤回的引擎改动 - -草案曾要求扩 `cfg()` 文法以支持 `cfg(mmu)` 这类能力谓词。**重估后撤回。** - -那条结论的出处是 K1/K2,而 K1/K2 测的是 **openarch 的 `AddressSpace`** —— 草案把它 -搬进了 openkal。逐个接口检查 openkal 有没有「存在但语义不同」的能力: - -| interface | 有 ③ 类语义轴吗 | -|---|---| -| `abort` | 无 | -| `stream` | seek / nonblock / vectored 都是**操作**(②类) | -| `memory` | 静态 arena vs 按需分页 = **容量**不是能力;分配失败到处都有定义 | -| `time` | monotonic vs wall 是**两种资源**,不是一个资源的两种语义 | -| `task` | ⚠️ 抢占 vs 协作**确实是** —— 但那是 D1 以后的事 | +--- -⇒ **core(abort + stream + memory)一条语义轴都不需要,①② 足够。** +## 5. ⭐「声称 ≠ 事实」问题:新机制消掉了大半 -而且 **triple 本身已经承载了大部分**:`riscv64-none-elf` 与 `riscv64-linux-gnu` 的 -区别里就包含了 MMU 用不用。今天已有的文法足以选后端。 +草案担心的是:后端写 `caps::seek = true` 而 `seek()` 永远失败。 +**§4 换成 ADL 之后,结构性的谎话已经说不出来了:** -⭐ **撤回后,本方案变成零引擎改动。** +> **你不能「声称有 seek」而不真的声明一个返回 `io_result` 的 `seek`。** +> 而声明了不定义,是链接错误。 -## 5. ⭐ caps 撒谎问题:四层防御,按强度排 +⇒ **声称与实现是同一个制品**,不再需要「同源生成」那套纪律,也不需要 `capabilities.toml`。 -**问题**:后端可以写 `caps::seek = true` 然后 `seek()` 永远失败。 -这是 K1/K2 的问题换了一层出现 —— caps 检查的是**有没有这个字段**,不是**它说的是不是真的**。 +### 5.1 ① 让「不支持」在类型系统里**不可表达**(仍然最强) -### 5.1 ① 让「不支持」在类型系统里**不可表达**(最强) +剩下的问题是**分类错误**,不是撒谎。判据不变: -如果一个操作可以「存在但永远失败」,**通常说明它被错误地合并了**。 +> **一个操作若能「存在但永远失败」,通常说明它被错误地合并了。** ⓘ MMU 就是这个的实例:`NoMmu::map()` 对非恒等映射返回 `false` —— 那**不是撒谎, 是这个抽象本来就不该把两族东西装进一个 concept**。当时的结论是把它挪到 `cfg` 轴。 -推广到 `seek`:socket 不能 seek,但那**不是后端撒谎,是名词错了**。 - -```cpp -// 不是:caps::seek = false -// 而是:句柄类型里根本没有那个能力位 -using console = kal::stream_of; // seek 不在 -using file = kal::stream_of; -kal::seek(c, 0); // ⚠️ 编译错误:重载要求 seek 位 -``` - -⇒ **能撒的谎少了一整类**,因为「`caps::x = true` 而 `x()` 无意义」在类型层面构造不出来。 - -### 5.2 ② caps 与实现**同源生成** - -不要让后端手写 caps。**一张表同时产出 caps 模块和源码选择**: +推广:socket 不能 seek,但那**不是后端撒谎,是名词错了** —— +所以 `fs` 的 descriptor 与 `net` 的 socket 各持自己的句柄类型(§2.3), +`kal::seek(socket, 0)` 因为**没有那个重载**而编译失败,不是因为返回错误。 -``` -backend/capabilities.toml ← 唯一来源 - ├─→ mcpp:generated= → openkal.stream.caps - └─→ 源码选择 → seek.cpp 编不编进去 -``` - -⇒ **撒谎要改表,而改表就把实现一起删了。** -`mcpp:generated=` 与源码选择**都是今天已有的指令**,不需要新机制。 - -### 5.3 ③ 双向 conformance:`false` 也要验 +### 5.2 ② 双向 conformance:`false` 也要验 -> `caps::seek == false` 的后端,**必须不导出 seek 符号** —— `nm` 可查。 +> 后端若不提供 `seek`,**必须真的不导出这个符号** —— `nm` 可查。 ⭐ 价值在于**它不是行为测试,是对制品的静态检查**:又快又不可能漏测。 -两侧都钉,caps 才从「声称」变成「事实」。 +在新机制下这条更强了:导出了符号 ⇒ ADL 就会找到 ⇒ 探测为真 ⇒ 与声称自动一致。 +**这条变成了「验证机制本身没被绕过」,而不是「验证后端没撒谎」。** -### 5.4 ④ 过程兜底 +### 5.3 ③ 过程兜底 conformance 结果进索引元数据:没过 seek 那组的后端,描述符里不允许声称。 这是唯一能约束「实现者根本不跑 conformance」的东西。 -### 5.5 ⚠️ 诚实的残余风险 +### 5.4 ⚠️ 诚实的残余风险 -**这四条都不证明行为。** 后端可以导出符号、通过 happy path、在某个输入上错。 +**以上都不证明行为。** 后端可以导出 `seek`、通过 happy path、在某个输入上错。 这是**每一份规范都有的残余风险**(POSIX 也一样),答案只能是 conformance 的覆盖度, 不存在语言层的解法。 -⇒ **顺序很重要**:先靠 ① 让错误分类不可表达,再靠 ② 让撒谎自毁,③④ 只是兜底。 -一上来就写更严的 conformance,是在给一个**本可以消除的问题**加检查。 +⇒ 但和草案相比,**残余从「结构 + 行为」缩小到只剩「行为」** —— 这正是把机制从 +「声明一个 bool」换成「声明一个函数」买到的东西。 --- @@ -358,8 +337,8 @@ conformance 结果进索引元数据:没过 seek 那组的后端,描述符里不 | | | |---|---| | 一组 `extern "C"` 定义 | §3 的清单,按你实现的 interface | -| 一个 `.caps` 模块 | ⚠️ **生成的**,不是手写的(§5.2) | -| conformance 通过记录 | 双向:声称有的能用,声称没有的**符号不存在** | +| 一个导出真实现的模块 | ⭐ **它就是能力声明**,不需要额外的 caps(§4) | +| conformance 通过记录 | 双向:声称有的能用,没提供的**符号不存在** | ### 6.2 最小实现:一个裸机 UART 后端 @@ -380,13 +359,9 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, uintptr_t n) { } ``` -```toml -# capabilities.toml —— caps 与源码选择的唯一来源 -[stream] -sequential = true -seek = false # ⇒ seek.cpp 不编进去,符号也不存在 -vectored = false -``` +⚠️ **没有第二份配置。** 这个后端不提供 `seek`,做法就是**不声明它** —— +`mcpp.toml` 里只有普通的包信息,`seek.cpp` 不存在,符号也不存在, +而消费者侧 `kal::HasSeek` 因此为假(§4.1)。 ### 6.3 后端怎么被选中 @@ -415,14 +390,25 @@ openkal-linux = "0.1" ### 7.1 应用直接用 ```cpp -import openkal.stream; +import openkal.uart; // 后端;接口随 reexport 而来 int main() { kal::write(kal::stdout(), "hello\n"); - if constexpr (kal::stream_caps::caps::vectored) { /* 用 writev 形状 */ } + + // 有就用,没有就走别的路 —— 探测是 ADL,不是查表 + if constexpr (kal::HasVectored) { /* writev 形状 */ } + else { /* 逐段写 */ } + + // 要求必须有:文案是规范自己写的 + static_assert(kal::HasSeek, + "this program needs seekable streams"); } ``` +⭐ **而且不写 `if constexpr` 也不会错**:直接调 `kal::seek(s, 0)` 在没有后端支持时 +就是**编译期报错并给出规范的原话**(§4.1 实测)—— 这是 §7 里问「能不能内置」的答案: +**能,而且是默认行为,不需要消费者做任何事。** + ⓘ **KA2 实测:换后端不重编应用** —— 同一个 `app.o` 换掉后端目标文件重链即成。 ### 7.2 类型化封装(C ABI 之上的零成本层) @@ -609,7 +595,8 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零 | 特性 | 解决什么 | ⚠️ 限制 | |---|---|---| | **modules** | 接口/caps 分离;**模块找不到 = 能力检查**,把链接期错误提前到编译期 | 需要 mcpp 的 `reexport`/provisions 承载 | -| **`constexpr` caps 描述符** | 能力是**值**不是符号 —— 绕开「`requires` 测不了缺失名字」 | — | +| ⭐ **ADL + `requires`** | ⓘ **非限定名**在模板里是 dependent 的 ⇒ 缺失时求值为 `false` 而不是硬错误。**能力探测不需要额外的数据结构,后端的模块接口就是声明** | ⚠️ **限定名**(`kal::seek`)仍是硬错误 —— 两者的差别是这套设计成立的全部基础 | +| ⭐ **兜底重载 + `static_assert`** | 没有后端时直接调用 ⇒ **编译期报错,文案是规范写的** | ⚠️ 兜底的**返回类型必须与真实现不同**,否则探测恒为真(ⓘ 实测踩过) | | **`if constexpr`** | 消费者按能力降级,未选中的分支**不实例化** | — | | **concepts** | 组合 caps;openhal 的多提供者共存 | ⚠️ ⓘ **只检查语法不检查语义**(K1/K2),③ 类能力不能用它 | | **非类型模板参数(能力位)** | 让「不支持」**在类型系统里不可表达**(§5.1) | — | @@ -620,7 +607,8 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零 | **`_Noreturn` / `[[noreturn]]`** | `kal_abort` 的控制流事实进类型 | — | | ~~virtual / vtable~~ | — | ⛔ 把结构体布局写进 ABI,「只增不改」保护不了 | | ~~exceptions / RTTI~~ | — | ⛔ 裸机整图关闭(mcpp 2026.8.19.4 起) | -| ~~`requires` 探测符号~~ | — | ⛔ ⓘ **实测:硬错误,不是 `false`** | +| ~~`requires` 探测**限定名**~~ | — | ⛔ ⓘ 实测:硬错误。**改用非限定 + ADL** | +| ~~`caps` 结构体 / `capabilities.toml`~~ | — | ⛔ 被 ADL 机制整个取代;且第二份配置文件违背生态整洁性 | --- @@ -688,6 +676,7 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零 |---|---|---| | **`openkal.namespace`**(取代 fs/net) | 「文件 / socket / UART 给你的都是 stream,只是命名方式不同」 | ① 触犯本文自己的 §5.1 规矩(caps 成为不相干能力并集);② URI 解析器**就是**模拟层,违反对下判据;③ 引用的 WASIp2 先例是**误读**(它分开资源种类,只共享 stream 类型) | | **扩 `cfg()` 文法支持 `cfg(mmu)`** | K1/K2 说「MMU 是能力轴不是契约」 | 那条结论是关于 **openarch** 的;openkal core 逐个接口查下来**一条 ③ 类语义轴都没有**,而且 triple 本身已承载大部分 | +| **`caps` 结构体 + `capabilities.toml`** | 以为「`requires` 测不了缺失的名字」⇒ 能力必须放进另一个可命名的东西 | ⓘ **只对限定名成立**。非限定 + ADL 在模板里是 dependent 的,缺失时求值为 `false`。⇒ 后端的模块接口本身就是能力声明,**两样东西整个删掉**,也不需要第二份配置 | | **「接口包 `reexport` 后端」** | 以为 mcpp 的承载件现成 | ⓘ `reexport` 是**向下游**传播,方向相反。正确接法是**后端 reexport 接口**(§4.1),恰好是该机制的本意 | ⭐ 三条的共同形状:**都是「我有一个漂亮的统一」,而漂亮的统一把两件本来不同的事合并了。** @@ -769,14 +758,25 @@ POSIX 至少规定了 `PIPE_BUF` 以内的原子性。**草案一个字没提。 三条撤回(§15)+ 六条开放问题(§16)里,有一个共同形状值得单独记: -⭐ **草案的错误全部是「一个漂亮的统一,把两件本来不同的事合并了」**: +草案的错误分两族。 + +**族一:一个漂亮的统一,把两件本来不同的事合并了。** - `openkal.namespace` 合并了「命名」与「资源种类」 - `cfg(mmu)` 把 openarch 的结论搬进 openkal -- 「两个堆」是没有合并该合并的(分配器) +- 「两个堆」是**没有**合并该合并的(分配器) 而 §5.1 给出的判据 —— **「一个操作若能『存在但永远失败』,说明它被错误地合并了」** —— -本来就能抓住前两条。 +本来就能抓住前两条。⚠️ **我只用它去检查别人的后端,没有用它检查自己的分解。** + +**族二:测了一种写法,把结论推广到了全部。** + +⚠️ `caps` 结构体 + `capabilities.toml` 那整套,建立在**一次实测**上: +「`requires { mcpp::runner("x") }` 是硬错误」。那次实测本身没错, +错在**结论的量词** —— 我写的是「C++ 语言内测不出来」,而真实情况是 +**「限定名测不出来」**。非限定 + ADL 一直是可以的。 + +⇒ 代价是**一整节设计 + 一份多余的配置文件格式**,而验证它只需要三行代码。 -⚠️ **我只用它去检查别人的后端,没有用它检查自己的分解。** -⇒ 判据要对**自己的设计**先跑一遍,再拿去当准入门槛。 +⭐ **两族合起来的规律**:一条实测能否定一个做法,**但否定不了一整类做法** —— +写下「X 做不到」之前,要先问「我测的是 X,还是 X 的某一种写法」。 From 25654d14a957a36cfe2d30f652bdb09134fceb85 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:34:43 +0800 Subject: [PATCH 06/10] docs(openkal): the backend owns the interface module name, and consumers declare both MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/docs/2026-08-20-openkal-design.md | 107 ++++++++++++++++++++-- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md index 60873b16..7a2a51e2 100644 --- a/.agents/docs/2026-08-20-openkal-design.md +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -255,28 +255,117 @@ export namespace kal { io_result seek(stream, long) { /* … */ } } **后端选择仍然只是条件依赖**(§4.3),整条链路**零新增配置、零新增引擎轴**。 -### 4.3 后端怎么被选中:只用 `mcpp.toml` +### 4.3 ⭐ 模块名的归属:后端提供「接口名」 + +这是全套设计里最容易写错的一处,而且草案写错过 —— 它让应用 `import openkal.uart;`, +**那等于把源码钉死在后端上**,正好推翻 openkal 的立身之本。 + +⚠️ 但它掩盖了一个真实约束。ⓘ **三条实测(mcpp 2026.8.19.4 / gcc 16.1.0)**: + +| | 结果 | +|---|---| +| 传递依赖的模块能不能 import | ✅ **能** —— app 只依赖 backend,可以 `import` iface 的模块 | +| **ADL 能不能到达未 import 的模块** | ⛔ **不能** —— `error: 'seek' was not declared in this scope` | +| ⇒ 所以后端的声明**必须在应用 import 的那个模块里** | — | + +⭐ **解法:`openkal.stream` 这个模块名由「后端」提供,接口包用另一个名字。** + +``` +openkal.abi.stream ← 接口包:类型、兜底重载、concepts +openkal.stream ← 由「后端」提供:export import openkal.abi.stream; + 真实现 +``` + +```cpp +// 接口包 +export module openkal.abi.stream; +export namespace kal { struct stream{…}; struct io_result{…}; + template unsupported_t seek(S, long) {…} + template concept HasSeek = …; } + +// 后端包 —— 它提供「接口名」 +export module openkal.stream; +export import openkal.abi.stream; +export namespace kal { io_result seek(stream, long) { … } } +``` + +```cpp +// 应用:后端在源码里无名 +import openkal.stream; +int main() { + kal::stream s{1}; + static_assert(kal::HasSeek); + return seek(s, 0).n; // ADL 找到后端的实现 +} +``` + +ⓘ **端到端跑通**(app 只写 `import openkal.stream;`,concept 为真,返回后端的值)。 + +⚠️ ⚠️ **一个必须踩过才知道的坑**:接口模块**不能**叫 `openkal.stream.abi`。 +ⓘ 实测直接 ninja 自环: + +``` +ninja: error: dependency cycle: gcm.cache/openkal.stream.gcm -> gcm.cache/openkal.stream.gcm +``` + +模块图把点号读成了层级关系。⇒ **ABI 模块名不能是接口名的点号延伸**, +`openkal.abi.stream` 可以,`openkal.stream.abi` 不行。 + +### 4.4 后端怎么被选中:只用 `mcpp.toml` ```toml # 后端包 openkal-uart [dependencies] -openkal-stream = { version = "0.1", reexport = true } # 把接口透给我的消费者 +openkal-abi = { version = "0.1" } # 它 export import 的那个 ``` ```toml -# 消费者:只写后端,按 target 选;接口随之而来 +# 消费者:只写后端,按 target 选 [target.'cfg(os = "linux")'.dependencies] openkal-linux = "0.1" [target.'cfg(os = "none")'.dependencies] openkal-uart = "0.1" ``` -源码 `import openkal.stream;` 两个 target 一字不改。 +源码 `import openkal.stream;` 两个 target 一字不改 —— ⓘ 这正是 KA1 测到的 +「同一份 `app.cppm`,零 `#if`,两个后端」。 + +⚠️ 两个后端同时进图 ⇒ 两个包都导出 `openkal.stream` ⇒ 模块名冲突。 +基数为 1(§12)使这成为用户错误,而且是**编译期**被发现的。 + +#### ⭐ 消费者要声明几个依赖:两个 + +```toml +[dependencies] +openkal-abi = "0.1" # ① 契约:我编程针对的那份规范 + +[target.'cfg(os = "linux")'.dependencies] +openkal-linux = "0.1" # ② 实现:可替换的那一半 +[target.'cfg(os = "none")'.dependencies] +openkal-uart = "0.1" +``` + +技术上 ① **可以省略**(后端已经把它拉进来了,模块也 `export import` 了)。 +**但不该省**,理由只有一条,而且是硬的: + +> ⭐ **① 是应用真正耦合的东西,而且它让「契约版本不匹配」变成解析期错误。** + +应用写 `openkal-abi = "0.2"`、后端只支持 `"0.1"` ⇒ **依赖解析当场失败**; +省掉 ① 的话,同一个问题要等到**编译期**才以一堆签名不匹配的形式冒出来。 + +而且这与生态里已被验证的形状一致:Rust 的 `embedded-hal`(trait 包)+ 板级包, +应用同时依赖两者;`log` + `env_logger` 也是同一个形状。 + +| | 只写后端 | ⭐ 契约 + 后端 | +|---|---|---| +| 行数 | 1 | 2 | +| 契约版本由谁定 | ⚠️ 后端 | **应用** | +| 版本不匹配何时暴露 | ⚠️ 编译期,一堆签名错误 | **解析期,一条消息** | +| 换后端要改几行 | 1 | 1(① 不动) | -⚠️ ⓘ 方向要注意:`reexport` 是**向下游**传播(`grpc` 把 protoc 透给用户), -所以是**后端 reexport 接口**,不是接口 reexport 后端 —— 草案写反过。 +⚠️ ① 看起来「声明了却没 import」—— 那是表象:应用 `import openkal.stream;` 时, +后端 `export import openkal.abi.stream;` 把它带了进来。**①的作用是钉版本,不是给 import 用。** -### 4.4 接口层面的缺失(①) +### 4.5 接口层面的缺失(①) 一整个 interface 不存在时,`import openkal.task;` **编译期就找不到模块**。 这一条不变,而且和 §4.1 是同一套失败语义:**编译期,点名,不是运行期返回值。** @@ -390,7 +479,7 @@ openkal-linux = "0.1" ### 7.1 应用直接用 ```cpp -import openkal.uart; // 后端;接口随 reexport 而来 +import openkal.stream; // ⭐ 只写接口名;提供它的是后端(§4.3) int main() { kal::write(kal::stdout(), "hello\n"); @@ -677,7 +766,7 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零 | **`openkal.namespace`**(取代 fs/net) | 「文件 / socket / UART 给你的都是 stream,只是命名方式不同」 | ① 触犯本文自己的 §5.1 规矩(caps 成为不相干能力并集);② URI 解析器**就是**模拟层,违反对下判据;③ 引用的 WASIp2 先例是**误读**(它分开资源种类,只共享 stream 类型) | | **扩 `cfg()` 文法支持 `cfg(mmu)`** | K1/K2 说「MMU 是能力轴不是契约」 | 那条结论是关于 **openarch** 的;openkal core 逐个接口查下来**一条 ③ 类语义轴都没有**,而且 triple 本身已承载大部分 | | **`caps` 结构体 + `capabilities.toml`** | 以为「`requires` 测不了缺失的名字」⇒ 能力必须放进另一个可命名的东西 | ⓘ **只对限定名成立**。非限定 + ADL 在模板里是 dependent 的,缺失时求值为 `false`。⇒ 后端的模块接口本身就是能力声明,**两样东西整个删掉**,也不需要第二份配置 | -| **「接口包 `reexport` 后端」** | 以为 mcpp 的承载件现成 | ⓘ `reexport` 是**向下游**传播,方向相反。正确接法是**后端 reexport 接口**(§4.1),恰好是该机制的本意 | +| **应用 `import openkal.uart;`** | 想让「接口随后端而来」 | ⛔ **把源码钉死在后端上**,推翻 openkal 的立身之本。ⓘ 而且掩盖了真约束:**ADL 到不了未 import 的模块**(实测)。正解是**后端提供接口名**(§4.3) | ⭐ 三条的共同形状:**都是「我有一个漂亮的统一」,而漂亮的统一把两件本来不同的事合并了。** 这与 §5.1 给出的判据是同一条 —— 只是那一节我用它去检查别人的后端,没有用它检查自己的分解。 From 8bd786fb4a7ec36fcc083cffd6329e7254201e79 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:43:18 +0800 Subject: [PATCH 07/10] docs(openkal): openkal IS the ABI, and the fragmentation risk is mechanically bounded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/docs/2026-08-20-openkal-design.md | 116 ++++++++++++++++++++-- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md index 7a2a51e2..ca155767 100644 --- a/.agents/docs/2026-08-20-openkal-design.md +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -271,20 +271,20 @@ export namespace kal { io_result seek(stream, long) { /* … */ } } ⭐ **解法:`openkal.stream` 这个模块名由「后端」提供,接口包用另一个名字。** ``` -openkal.abi.stream ← 接口包:类型、兜底重载、concepts -openkal.stream ← 由「后端」提供:export import openkal.abi.stream; + 真实现 +openkal.decl.stream ← 接口包:类型、兜底重载、concepts +openkal.stream ← 由「后端」提供:export import openkal.decl.stream; + 真实现 ``` ```cpp // 接口包 -export module openkal.abi.stream; +export module openkal.decl.stream; export namespace kal { struct stream{…}; struct io_result{…}; template unsupported_t seek(S, long) {…} template concept HasSeek = …; } // 后端包 —— 它提供「接口名」 export module openkal.stream; -export import openkal.abi.stream; +export import openkal.decl.stream; export namespace kal { io_result seek(stream, long) { … } } ``` @@ -300,7 +300,7 @@ int main() { ⓘ **端到端跑通**(app 只写 `import openkal.stream;`,concept 为真,返回后端的值)。 -⚠️ ⚠️ **一个必须踩过才知道的坑**:接口模块**不能**叫 `openkal.stream.abi`。 +⚠️ ⚠️ **一个必须踩过才知道的坑**:接口模块**不能**叫 `openkal.stream.decl`。 ⓘ 实测直接 ninja 自环: ``` @@ -308,14 +308,14 @@ ninja: error: dependency cycle: gcm.cache/openkal.stream.gcm -> gcm.cache/openka ``` 模块图把点号读成了层级关系。⇒ **ABI 模块名不能是接口名的点号延伸**, -`openkal.abi.stream` 可以,`openkal.stream.abi` 不行。 +`openkal.decl.stream` 可以,`openkal.stream.decl` 不行。 ### 4.4 后端怎么被选中:只用 `mcpp.toml` ```toml # 后端包 openkal-uart [dependencies] -openkal-abi = { version = "0.1" } # 它 export import 的那个 +openkal = { version = "0.1" } # 它 export import 的那个 ``` ```toml @@ -336,7 +336,7 @@ openkal-uart = "0.1" ```toml [dependencies] -openkal-abi = "0.1" # ① 契约:我编程针对的那份规范 +openkal = "0.1" # ① 契约:openkal 本身就是那份规范 [target.'cfg(os = "linux")'.dependencies] openkal-linux = "0.1" # ② 实现:可替换的那一半 @@ -349,7 +349,7 @@ openkal-uart = "0.1" > ⭐ **① 是应用真正耦合的东西,而且它让「契约版本不匹配」变成解析期错误。** -应用写 `openkal-abi = "0.2"`、后端只支持 `"0.1"` ⇒ **依赖解析当场失败**; +应用写 `openkal = "0.2"`、后端只支持 `"0.1"` ⇒ **依赖解析当场失败**; 省掉 ① 的话,同一个问题要等到**编译期**才以一堆签名不匹配的形式冒出来。 而且这与生态里已被验证的形状一致:Rust 的 `embedded-hal`(trait 包)+ 板级包, @@ -363,7 +363,45 @@ openkal-uart = "0.1" | 换后端要改几行 | 1 | 1(① 不动) | ⚠️ ① 看起来「声明了却没 import」—— 那是表象:应用 `import openkal.stream;` 时, -后端 `export import openkal.abi.stream;` 把它带了进来。**①的作用是钉版本,不是给 import 用。** +后端 `export import openkal.decl.stream;` 把它带了进来。**①的作用是钉版本,不是给 import 用。** + +#### ⚠️ 命名:丑的那一半要落在实现者身上 + +**openkal 本身就是接口/ABI,包名不该再带 `-abi`。** + +| 谁写 | 名字 | 谁看得到 | +|---|---|---| +| 应用 | `import openkal.stream;` · `openkal = "0.1"` | **所有人** —— 干净 | +| 后端作者 | `export import openkal.decl.stream;` | **只有实现者** | + +⇒ 两个模块名是语言逼出来的(§4.3),但**限定词只出现在实现者那一侧**。 + +#### ⚠️ 后端拥有应用可见的名字 ⇒ 碎片化风险,靠规范 + conformance 机械地封住 + +这是这套形状唯一的真代价:`openkal.stream` 由后端提供,理论上后端可以往里塞 +非标准的东西,而应用**察觉不到自己用了厂商扩展**。 + +⭐ 但**后端能塞的东西是有界的**,而且边界是语言给的: + +| 后端**不能** | 因为 | +|---|---| +| 重定义 `stream` / `io_result` / `unsupported_t` | ⓘ **实测被编译器拒**:`redeclaring 'struct kal::io_result@openkal.decl.stream' in module 'openkal.stream' conflicts with import` | +| 改 concepts 的语义 | 同上 | +| 改兜底重载 | 同上 | +| **只能**:加 spec 列出的那些函数的实现,**或额外的重载** | ← 唯一的自由度 | + +⇒ **规范只需要封住最后一行**,而它是**静态可查**的: + +> **SPEC**:`openkal.stream` 导出的名字集合**必须等于** spec 列表 ∩ 该后端实现的能力。 +> 厂商扩展必须放在**另一个模块名**里(`vendor.foo.stream`),应用要用就得显式 import 它 —— +> **于是「我用了扩展」在源码里是可见的。** +> +> **conformance**:dump 后端模块的导出名集合,与 spec 列表**逐条 diff**。多一个名字 = 不通过。 + +⚠️ 还要 diff **签名**,不只是名字:后端把 `seek(stream, long)` 写成 +`seek(stream, unsigned long)` 时 ADL 仍会经隐式转换选中它,而语义可能不同。 + +⭐ 这条比 §5 的其它防御更强,因为它**不是行为测试** —— 名字集合是制品的静态属性。 ### 4.5 接口层面的缺失(①) @@ -869,3 +907,61 @@ POSIX 至少规定了 `PIPE_BUF` 以内的原子性。**草案一个字没提。 ⭐ **两族合起来的规律**:一条实测能否定一个做法,**但否定不了一整类做法** —— 写下「X 做不到」之前,要先问「我测的是 X,还是 X 的某一种写法」。 + +--- + +## 18. 第二轮综合 review 新增的两条 + +### 18.1 ⭐ 基数是「每个 interface 一个实现」,不是「每个程序一个后端」 + +草案 §12 说 openkal 的基数是 1,含糊在于**1 个什么**。澄清: + +```toml +[target.'cfg(os = "none")'.dependencies] +openkal-uart = "0.1" # 提供 openkal.stream +openkal-arena = "0.1" # 提供 openkal.memory +``` + +**一个程序可以从不同提供者拿不同的 interface** —— 这是好事,而且是 §2 资源分解的 +自然结果。冲突只发生在**同一个 interface 有两个提供者**时(两个包都导出 +`openkal.stream`),那是模块名冲突,**编译期报错**。 + +⇒ 「基数 1」应当读作:**每个 interface 的实现是 1 个**。契约形态(C ABI)的论据不变。 + +### 18.2 ⚠️ 兜底重载太贪心,文案会张冠李戴 + +```cpp +template unsupported_t seek(S, long) { + static_assert(always_false, "…no seekable streams…"); +} +``` + +`S` 无约束 ⇒ `kal` 里**任何**类型调 `seek` 都落到这里。 +`seek(some_socket, 0)` 会得到「no seekable **streams**」—— 名词错了。 + +**修法**:兜底要约束到它该管的类型,或者文案改成不带具体名词的。 + +```cpp +template requires std::same_as +unsupported_t seek(S, long) { … } +``` + +⚠️ 这条小,但它是 §5.1 那条判据的又一次应用:**兜底重载的适用范围也是一种「分类」**, +分类过宽,诊断就会指向错误的地方。 + +--- + +## 19. 设计现状小结(第二轮 review 后) + +| 维度 | 状态 | +|---|---| +| **位置无关**(上/下 libc) | ✅ 由不透明句柄承载,ⓘ 三种后端实测 | +| **划分依据** | ✅ 资源种类;⚠️ 曾塌缩过头(namespace),已撤回 | +| **core 边界** | ✅ abort + stream + memory,判别式是「实现 vs 模拟」 | +| **能力探测** | ✅ ADL + 兜底重载,ⓘ 真模块上三行为同时成立;**零额外配置** | +| **模块名归属** | ✅ 后端提供接口名(语言逼出来的),⚠️ 碎片化靠 spec + 名字集合 diff 封住(静态可查) | +| **依赖形状** | ✅ 契约 + 后端两条,契约那条用来**钉版本** | +| **引擎改动** | ✅ **零** | +| **对下/对上判据** | ✅ 双向且可数(数桥接行数) | +| ⚠️ **开放问题** | §16 六条 + §18 两条,**全部需要 SPEC 表态** | +| ⚠️ **最大风险** | 不是技术 —— 是 **D0 的门:有没有第三方来实现第三个后端** | From 5f5c83145004dab95ac3f55ce61b479822cfcdb4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:50:47 +0800 Subject: [PATCH 08/10] docs(openkal): add a complete Linux reference implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/docs/2026-08-20-openkal-design.md | 191 +++++++++++++++++++++- 1 file changed, 190 insertions(+), 1 deletion(-) diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md index ca155767..508f9729 100644 --- a/.agents/docs/2026-08-20-openkal-design.md +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -837,6 +837,17 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零 ⚠️ 这条部分可 conformance 化:检查固件里 `sbrk` 的消费者是不是只有一个。 +### 16.1b ⚠️ 短写:`kal_stream_write` 写不全时怎么办(§20.3 逼出来的) + +写参考实现时立刻撞上:`::write(2)` 可以短写。SPEC 必须选一边: + +| | 后果 | +|---|---| +| **写全或报错**(推荐) | 循环在**后端**里写一次 | +| 允许短写 | ⚠️ **每个调用方**都要自己写循环 —— 这正是 POSIX 让无数程序出错的地方 | + +⇒ 建议规定「写全或报错」,短写只在 `kal_stream_write_some`(若真需要)里出现。 + ### 16.2 ⚠️ 错误集合的封闭性 草案说「封闭 `enum class`,不透传 errno」。但 POSIX 有约 130 个 errno, @@ -964,4 +975,182 @@ unsupported_t seek(S, long) { … } | **引擎改动** | ✅ **零** | | **对下/对上判据** | ✅ 双向且可数(数桥接行数) | | ⚠️ **开放问题** | §16 六条 + §18 两条,**全部需要 SPEC 表态** | -| ⚠️ **最大风险** | 不是技术 —— 是 **D0 的门:有没有第三方来实现第三个后端** | +| ⚠️ **最大风险** | 不是技术 —— 是 **D0 的门:有没有第三方来实现第三个后端**。⭐ 缓解手段是 §20 的官方 `openkal-linux` 完整参考实现:**降低门槛,但不移动门** | + +--- + +## 20. 官方参考实现:`openkal-linux`(完整) + +⭐ **它有两个身份**:一个**当天可用**的后端,和**其它实现者要抄的那份样板**。 + +⚠️ 它**不能移动 D0 的门**(门是「有第三方实现了第三个后端」),但它把门**变得够得着** —— +没有可抄的样板时,第三方要同时猜形状和写实现。 + +### 20.1 ⭐ 写这份实现时才发现的一条:core 操作根本不需要 ADL + +| | 声明在哪 | 后端提供什么 | 探测机制 | +|---|---|---|---| +| **core 操作**(write/read/alloc/abort) | 接口包 `extern "C"` + C++ 封装 | **只有 C 函数的定义** | **不需要** —— core 的定义就是「一定在」 | +| **可选能力**(seek/vectored/…) | 后端声明 C++ 重载 | 声明 + 定义 | **ADL**(§4.1) | + +⇒ ADL 那套只服务**可选能力**。core 走最简单的路:接口声明,后端定义,缺了就是链接错误。 + +### 20.2 包结构 + +``` +openkal-linux/ +├── mcpp.toml +└── src/ + ├── stream.cppm export module openkal.stream; ← 提供接口名 + ├── stream.cpp extern "C" 定义 + ├── memory.cppm export module openkal.memory; + ├── memory.cpp + ├── abort.cppm export module openkal.abort; + └── abort.cpp +``` + +```toml +[package] +name = "openkal-linux" +version = "0.1.0" + +[dependencies] +openkal = "0.1" # 契约 + +[target.'cfg(not(linux))'.build] +# 这个后端只在 linux 上有意义;别的 target 上它不该被选中 +``` + +### 20.3 `openkal.stream` + +```cpp +// src/stream.cppm —— 提供应用可见的名字,自己不加任何非标准的东西 +export module openkal.stream; +export import openkal.decl.stream; +// core 操作无需在此声明:它们是 openkal.decl.stream 里的 extern "C" + 封装。 +// 这个后端也不提供 seek —— 见 20.6,那不是疏漏。 +``` + +```cpp +// src/stream.cpp +#include +#include +import openkal.decl.stream; + +extern "C" { + +kal_stream kal_stdin (void) { return kal_stream{0}; } +kal_stream kal_stdout(void) { return kal_stream{1}; } +kal_stream kal_stderr(void) { return kal_stream{2}; } + +kal_io_result kal_stream_write(kal_stream s, const void* buf, uintptr_t n) { + auto* p = static_cast(buf); + uintptr_t done = 0; + while (done < n) { + ssize_t r = ::write(static_cast(s.h), p + done, n - done); + if (r < 0) { + // ⚠️ EINTR 必须重试。漏掉它的后端在有信号的系统上会随机短写, + // 而这类 bug 在测试里几乎不出现。 + if (errno == EINTR) continue; + return { done, kal_from_errno(errno) }; + } + if (r == 0) break; + done += static_cast(r); + } + return { done, 0 }; +} + +kal_io_result kal_stream_read(kal_stream s, void* buf, uintptr_t n) { + for (;;) { + ssize_t r = ::read(static_cast(s.h), buf, n); + if (r < 0) { if (errno == EINTR) continue; return { 0, kal_from_errno(errno) }; } + return { static_cast(r), 0 }; // 短读是正常的,不重试 + } +} + +int32_t kal_stream_flush(kal_stream) { return 0; } // 裸 fd 无用户态缓冲 + +} +``` + +⚠️ **写这段逼出了一个 SPEC 必须回答的问题**(§16 没覆盖): + +> `kal_stream_write` 返回**短写**,还是**写全或报错**? + +上面选了「循环到写全」。若 SPEC 选另一边,**每个调用方都要自己写这个循环** —— +这正是 POSIX 让无数程序出错的地方。⇒ **建议 SPEC 规定「写全或报错」,短写只在 +`kal_stream_write_some`(如果需要)里出现。** + +### 20.4 `openkal.memory` —— 演示 §16.1 的规则 + +```cpp +// src/memory.cpp +#include + +extern "C" { +// ⭐ 建在 libc 分配器之上,不是与它并列 —— §16.1 的规则,这里是它的正面示例。 +void* kal_alloc(uintptr_t size, uintptr_t align) { + if (align <= alignof(max_align_t)) return ::malloc(size); + return ::aligned_alloc(align, (size + align - 1) / align * align); +} +// sized-free:这个方向丢掉 size 是零成本的(§16.6) +void kal_free(void* p, uintptr_t, uintptr_t) { ::free(p); } +} +``` + +### 20.5 `openkal.abort` + +```cpp +// src/abort.cpp +#include +#include + +extern "C" { +[[noreturn]] void kal_abort(const char* msg, uintptr_t len) { + if (msg && len) { ssize_t r = ::write(2, msg, len); (void)r; } + ::abort(); +} +[[noreturn]] void kal_exit(int32_t code) { ::_exit(code); } +} +``` + +⚠️ `_exit` 而不是 `exit`:`exit` 会跑 atexit 与静态析构,而 `kal_exit` 的契约是 +「立刻结束」。这类差别**必须写进 SPEC**,否则两个后端的语义会悄悄分叉。 + +### 20.6 ⭐ 参考实现验证了 fs/net 的分解(§2.3) + +写 Linux 后端时会立刻撞上一件事: + +> **Linux 上「能不能 seek」是每个句柄的属性,不是后端的属性。** +> `lseek(fd)` 对普通文件成功,对管道 `ESPIPE`。 + +⇒ 如果 `openkal.stream` 有 `seek`,Linux 后端**无法诚实回答** —— +声称有,则对管道永远失败(**正是 §5.1 的「存在但永远失败」反模式**); +声称没有,则文件用不了。 + +⭐ **而 §2.3 的分解让这个问题不存在**:seek 属于 `openkal.fs` 的 descriptor 类型, +`openkal.stream` 压根没有它。**参考实现独立地证实了那次撤回是对的。** + +⇒ **这是「写一份完整实现」最大的价值:它是唯一能发现分解错误的方法, +而且比 conformance 更早。** + +### 20.7 它作为样板教什么 + +| 样板里的模式 | 其它实现者照抄什么 | +|---|---| +| `stream.cppm` 只有 `export import`,不加任何东西 | **不要往标准模块名里塞私货**(§4.4) | +| EINTR 循环 | 每个后端都要处理自己平台的「被打断」 | +| `kal_alloc` 走 `malloc` | §16.1:有 libc 分配器就建在它之上 | +| `kal_from_errno` 是一张**表** | **映射 ≠ 模拟**(§3.1) | +| 没有 `seek` | 能力缺失就是**不声明**,不是声明后返回错误 | +| `_exit` 而非 `exit` | 语义细节要向 SPEC 对齐,不要凭直觉 | + +### 20.8 对 D0 的影响:降低门槛,不移动门 + +| | | +|---|---| +| D0 的门 | **有第三方实现了第三个后端** —— 不变 | +| 官方 linux 后端做的事 | 把「猜形状 + 写实现」减成**只写实现** | +| ⚠️ 不做的事 | 它**不算**第三方后端,也不算第三个后端(linux/bare 是官方的两个) | + +⇒ 判据仍然是**别人来不来**,而这份实现让「来」这件事从一个季度变成一个周末。 From cf5924e8fe07d8514971d93cf82dfa75d5fbab54 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:51:57 +0800 Subject: [PATCH 09/10] docs(openkal): module naming is normative, and decl is not interchangeable with impl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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.` 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. --- .agents/docs/2026-08-20-openkal-design.md | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md index 508f9729..4b134eff 100644 --- a/.agents/docs/2026-08-20-openkal-design.md +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -310,6 +310,39 @@ ninja: error: dependency cycle: gcm.cache/openkal.stream.gcm -> gcm.cache/openka 模块图把点号读成了层级关系。⇒ **ABI 模块名不能是接口名的点号延伸**, `openkal.decl.stream` 可以,`openkal.stream.decl` 不行。 +#### ⭐ 模块命名是**规范条款**,不是风格 + +| 名字 | 谁提供 | 谁写 | +|---|---|---| +| `openkal.` | **后端** | 应用 `import` 它 | +| `openkal.decl.` | **接口包** | **只有后端作者** `export import` 它 | + +**必须入规范**,三条理由都是硬的: + +1. ⭐ 每个后端都要 `export import` **这个确切的名字** —— 名字本身就是契约的一部分; + 名字自由 = 后端没法照着规范写。 +2. ⓘ 「后端不能重定义接口类型」那条保证(实测 `conflicts with import`) + **只在所有后端 import 同一个模块时成立**。 +3. conformance 要 diff 导出名集合,得知道哪些来自共享模块、哪些是后端自己加的。 + +**为什么是 `decl`**(排除掉的): + +| | | +|---|---| +| `openkal.impl.*` | ⛔ **语义反了** —— 那是接口包,不是实现;实现提供的是 `openkal.*` | +| `openkal.abi.*` | ⛔ openkal 本身就是 ABI,冗余 | +| `openkal.core.*` | ⛔ 与 §2.1 的「core 接口集」撞词 | +| `openkal.spec.*` | ⚠️ 可用,但读者会以为里面是规范**正文** | +| ⭐ `openkal.decl.*` | 无歧义:里面就是声明 | + +⚠️ **规范必须连同理由一起写**:读者会很自然地选 `openkal.stream.decl`(点号延伸), +而那个 ⓘ 实测直接自环(§4.3)。只写「必须这么叫」不写为什么,第一个实现者就会卡住。 + +⚠️ **一个被否掉的简化**:把所有声明合并成单个 `openkal` 模块,后端各自 +`export import openkal;`。少一层命名,但**破坏「每个 interface 独立版本」** —— +只提供 stream 的后端会把 task/fs 的声明一起拖进来,大模块对裸机的编译代价也不友好。 +⇒ **每个 interface 一个 `openkal.decl.`。** + ### 4.4 后端怎么被选中:只用 `mcpp.toml` ```toml From 0d899d5c0cd3933708739287673ad4288aa49b28 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 03:08:45 +0800 Subject: [PATCH 10/10] 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. --- .agents/docs/2026-08-20-openkal-design.md | 13 +- .../2026-08-20-openkal-implementation-plan.md | 186 ++++++++++++++++++ 2 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 .agents/docs/2026-08-20-openkal-implementation-plan.md diff --git a/.agents/docs/2026-08-20-openkal-design.md b/.agents/docs/2026-08-20-openkal-design.md index 4b134eff..0b7063e1 100644 --- a/.agents/docs/2026-08-20-openkal-design.md +++ b/.agents/docs/2026-08-20-openkal-design.md @@ -1,7 +1,16 @@ # openkal 设计方案:通用内核 ABI 规范 -**状态**:设计草案,**未实施**。对应第一阶段计划 §7 的 **D0/D1**,而 D0 的门不是技术判据 —— -是**「有第三方实现了第三个后端」**。本文的价值在于**不要在 D0 就把路堵死**。 +**状态**:**0.1 已实施并发布**。规范与声明模块在 +[`mcpplibs/openkal`](https://github.com/mcpplibs/openkal),Linux 参考实现在 +[`mcpplibs/openkal-linux`](https://github.com/mcpplibs/openkal-linux),两者均为 `0.1.0`。 +实施计划与结果见 +[`2026-08-20-openkal-implementation-plan.md`](2026-08-20-openkal-implementation-plan.md)。 + +对应第一阶段计划 §7 的 **D0**;D0 的门不是技术判据 —— +是**「有第三方实现了第三个后端」**,参考实现降低门槛而不移动门。 + +⚠️ **本文与规范正文的关系**:本文记录**推导过程**,包含被撤回的方案与其理由; +规范正文(`openkal/SPEC.md`)只记录**结论**。两者冲突时以规范正文为准。 **证据来源**:标注 ⓘ 的是本机实测(载荷版本写在旁边),其余是设计主张。 调研出处见 [`2026-08-18-freestanding-baremetal-analysis.md`](2026-08-18-freestanding-baremetal-analysis.md)。 diff --git a/.agents/docs/2026-08-20-openkal-implementation-plan.md b/.agents/docs/2026-08-20-openkal-implementation-plan.md new file mode 100644 index 00000000..3cbdc90b --- /dev/null +++ b/.agents/docs/2026-08-20-openkal-implementation-plan.md @@ -0,0 +1,186 @@ +# openkal: implementation plan and outcome + +This document records the plan by which openkal 0.1 was implemented, the +dependencies among its tasks, the criteria applied to each, and the result. It +accompanies the design analysis in +[`2026-08-20-openkal-design.md`](2026-08-20-openkal-design.md) and the +specification itself, which is maintained in the `mcpplibs/openkal` repository. + +## 1. Deliverables + +| Repository | Contents | Version | +| --- | --- | --- | +| `mcpplibs/openkal` | the specification, the modules that declare it, the surface checker, and a substitution example | 0.1.0 | +| `mcpplibs/openkal-linux` | the reference implementation for Linux, its conformance suite, and an example | 0.1.0 | +| `mcpplibs/mcpp-index` | descriptors for both packages | pull request 220 | + +Both packages are mirrored to GitCode, and the mirrored archives were verified +to be byte-identical to those served by GitHub. + +## 2. Task dependencies + +The work divides into five groups. Groups A and B are independent of each +other; C depends on both; D and E follow C. + +``` +A. specification text ────┐ + ├──► C. reference implementation ──► D. conformance +B. declaration modules ───┘ │ + └► E. distribution +``` + +The order is not arbitrary. Writing the reference implementation before +completing the specification would have produced a specification describing one +implementation, which clause 7.1 of the specification exists to prevent. +Deferring the reference implementation until after distribution would have +deferred the discovery reported in section 4.3. + +| Group | Tasks | Depends upon | +| --- | --- | --- | +| A | scope, interface inventory, capability model, conformance procedure | the design analysis | +| B | `openkal.decl.types`, `.abort`, `.stream`, `.memory` | A | +| C | `openkal.abort`, `.stream`, `.memory` for Linux | A, B | +| D | behavioural suite, absence assertions, exported-surface comparison | C | +| E | repositories, tags, mirrors, index descriptors, verification | D | + +## 3. Criteria applied + +The following criteria were fixed before implementation and applied to each +decision. Where a decision satisfied one criterion at the expense of another, +the resolution is recorded. + +### 3.1 Architecture + +An interface is the unit of provision and of versioning. An implementation +provides an interface in whole or not at all, and an interface that is not +provided is absent as a module rather than present and refusing. + +The consequence for the module layout is stated in clause 4 of the +specification: the implementation owns the name a consumer imports, because +argument-dependent lookup does not reach a module the translation unit has not +imported. The alternative arrangement, in which the specification package owns +that name, was rejected because it would have made optional capabilities +undetectable. + +### 3.2 Stability + +The layout of every structure is frozen at 0.1, and the evolution rule admits +new declarations while excluding changes to existing ones. The C surface carries +no version in its symbol names; the specification records this as an unsettled +matter rather than concealing it. + +The reference implementation retries interrupted operations rather than +reporting them. The alternative produces short transfers on any system that +delivers asynchronous notifications, and such a defect is unlikely to be +reproduced by a test suite. + +### 3.3 Simplicity + +The capability mechanism uses argument-dependent lookup and a fallback overload. +An earlier design used a record of capability flags together with a separate +configuration file, and both were removed. A record can disagree with the code +it describes; a declaration cannot. The removal also eliminated a second +configuration format from the ecosystem, in which every other fact about a +package resides in `mcpp.toml`. + +### 3.4 User experience + +A consumer that calls an operation the implementation does not provide is +rejected during compilation, and the diagnostic carries the wording the +specification supplies. This is the default behaviour and requires nothing of +the consumer: no capability test, no configuration, and no annotation. + +A consumer that wishes to adapt rather than fail uses the concept the interface +provides, which evaluates to false when the operation is absent. + +### 3.5 Compatibility + +A consumer declares two dependencies. The second selects an implementation; the +first fixes the version of the contract, and converts a mismatch between +consumer and implementation into a failure of dependency resolution rather than +a collection of signature errors at compile time. + +Neither package raises the index floor, and neither requires a version of mcpp +beyond one already published. + +### 3.6 Portability + +The handle is one machine word and opaque. An implementation stores a +descriptor, an operating-system handle, a pointer to a driver structure, or a +capability index, and none is required to maintain a translation table. This is +the property that permits an implementation to be placed above a C library, +beneath one, or without one. + +The specification does not assume a process model, a division between +privileged and unprivileged execution, or a namespace shared by all callers. + +### 3.7 Consistency + +The specification adopts the vocabulary already established in this ecosystem: +capability absence is expressed at compile time, backend selection is a +conditional dependency, and no new axis is introduced into the build system. The +implementation of openkal required no change to mcpp. + +### 3.8 Upgrade without disruption + +Version 0.1 introduces two packages and modifies none. No existing project is +affected, and the criterion is satisfied trivially. It is recorded so that +subsequent versions, which will not satisfy it trivially, are measured against +it. + +## 4. Verification performed + +### 4.1 Language constructs + +Exporting `extern "C"` declarations from a module, and calling them from a +consumer that imports the module, was verified before the interface was written. + +### 4.2 Substitution + +One application source was compiled against two implementations, one writing to +descriptors and one discarding every transfer. The observable behaviour differed +and the source did not: its checksum was taken before the first build and +compared after the second. The example is retained in the specification +repository and is exercised by its continuous integration. + +### 4.3 A decomposition confirmed by implementation + +Writing the Linux implementation confirmed clause 6.3 independently of the +reasoning that produced it. On Linux, whether a stream can be repositioned is a +property of the individual descriptor: the same implementation succeeds for a +regular file and fails for a pipe. Had `openkal.stream` offered positioning, the +implementation could have neither claimed it honestly nor withheld it usefully. + +This is the argument for writing a complete reference implementation rather than +a sketch. A decomposition error of this kind is not visible in the +specification text, and a conformance suite would find it later. + +### 4.4 The surface checker + +The exported-surface comparison required by clause 9.3 was verified in both +directions: it accepts the reference implementation, and it rejects the same +implementation after an unspecified name is added. The negative direction is the +one that establishes the check is not vacuous, and an earlier version of it was +vacuous, comparing a set of C++ symbols that inline functions never emit. + +### 4.5 Conformance + +The suite verifies that the operations provided behave as specified and that the +operation not provided is absent, the latter as a compile-time assertion. + +## 5. Matters deferred + +| Matter | Reason | +| --- | --- | +| `openkal.time`, `.task`, `.fs`, `.net`, `.channel` | 0.1 specifies the core set; the remaining names are reserved | +| Concurrency upon one handle | unavoidable once `openkal.task` is specified, and not before | +| Symbol versioning | the evolution rule protects the interface by prohibiting change; an ecosystem that outgrows the prohibition will need a mechanism this version does not define | +| A shared conformance package | the suite presently resides with the implementation; a reusable package requires a mechanism for a test package to be compiled against an implementation chosen by a third project | + +## 6. The gate that governs what follows + +The design analysis records that the criterion for continuing beyond D0 is not +technical: it is whether a third party implements a third backend. The reference +implementation does not satisfy that criterion and does not alter it. Its effect +is to reduce the work such a party must perform from inferring a shape and +writing an implementation to writing an implementation.