Skip to content

Commit 910ec2b

Browse files
committed
docs(openkal): retract two decisions after review, and record six open questions
Two things the design got wrong, both retracted with the reasoning that made them look right at the time: * `openkal.namespace` replacing fs and net. It violated this document's own §5.1 rule (a stream whose caps are the union of file and socket operations is precisely the 'present but useless' antipattern), it required every backend to carry a URI parser — which is an emulation layer by the downward admission criterion — and the WASIp2 precedent it cited was a misreading: WASIp2 separates resource KINDS and shares only the stream type. * Extending cfg() with capability predicates. That conclusion was about openarch's AddressSpace; going through openkal interface by interface, core has no semantic axis at all, and the triple already carries most of what cfg(mmu) would have. The design is now a zero-engine-change proposal. Also corrects the module wiring: `reexport` propagates DOWNSTREAM, so an interface package cannot use it to reach a backend the consumer chose. The backend reexports the interface instead, which is what that mechanism is for. Six open questions the draft did not take a position on, led by one that is measured rather than theoretical: picolibc's vfprintf references free, so routing operator new to kal_alloc while printf keeps picolibc's malloc puts two allocators on the same RAM. The spec has to require that kal_alloc be built over a libc allocator where one exists, not beside it.
1 parent 708e70f commit 910ec2b

1 file changed

Lines changed: 195 additions & 22 deletions

File tree

.agents/docs/2026-08-20-openkal-design.md

Lines changed: 195 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@ hosted 后端把 `kal_stream_write` 转发到 `::write(2)`(在 libc 之上);
6161
| **`openkal.memory`** | 一块内存区域 || 静态 arena 上的 bump allocator 是一个**实现** |
6262
| `openkal.time` | 一个时间源 || ⚠️ 不走的计数器**不是**时钟 —— 会让超时静默失效 = **模拟** |
6363
| `openkal.task` | 一个执行上下文 || 需要调度 + 上下文切换(那是 openarch) |
64-
| `openkal.namespace` | 名字 → 资源 || 需要一个命名权威 |
64+
| `openkal.fs` | 一个 **descriptor**(自有句柄类型) || 需要一个命名权威 |
65+
| `openkal.net` | 一个 **socket**(自有句柄类型) || 同上 |
6566
| `openkal.channel` | 一条消息通道 |||
6667

68+
**`stream` 是共享货币,不是统一入口**:`fs` 的 descriptor 与 `net` 的 socket 各有
69+
自己的句柄类型和自己的操作,但**都能产出 `openkal.stream`**。往哪写这件事对文件 /
70+
socket / UART 是同一套代码;打开它们不是。
71+
6772
### 2.2 ⭐ core 的判别式:实现 vs 模拟
6873

6974
> **假实现会让上层「静默地错」的 ⇒ 模拟;只是「容量小 / 会失败」的 ⇒ 实现。**
@@ -74,21 +79,48 @@ hosted 后端把 `kal_stream_write` 转发到 `::write(2)`(在 libc 之上);
7479
**「这块 MCU 没有堆」是错的命题**:只要有 RAM,堆就是实现出来的;
7580
上层不关心 openkal 底层怎么做到。
7681

77-
### 2.3 ⚠️ `fs``net` 不是接口
82+
### 2.3 ⚠️ 一个被起草后撤回的分解:`openkal.namespace`
83+
84+
草案曾把 `fs``net` 消掉,换成一个通用的 `openkal.namespace`(名字 → 资源),
85+
理由是「文件 / TCP 连接 / 管道 / 串口给你的都是 stream,只是命名方式不同」。
86+
87+
**重估后撤回。三条攻击全部成立:**
88+
89+
**① 它触犯本文自己的 §5.1 规矩。** 「一个操作若能『存在但永远失败』,说明它被错误地
90+
合并了」—— 而把文件和 socket 都塞进一个 `stream`,`stream` 的 caps 就成了
91+
**互不相干能力的并集**(seek/size/truncate/sync 对上 shutdown/peer/nodelay),
92+
每个后端对其中大多数说 `false`**正是那个反模式,换了个地方出现。**
93+
94+
**`namespace` 需要一个 URI 解析器,那是模拟层。** `kal_namespace_open("tcp://…")`
95+
要求**每个后端都能解析 scheme**:只有 UART 的后端也得解析并拒绝 `tcp://`;
96+
合法 scheme 集合无界、不可发现;错误是字符串形状的。
97+
⚠️ **直接违反对下判据**(四后端自然实现、不需模拟层),而且比 POSIX 还差 ——
98+
POSIX 至少 `open()``socket()+connect()` 是分开的类型化调用。
7899

79-
它们在资源分解下**溶解**:
100+
**③ 引用的先例是错的。** 草案称「这是 WASIp2 收敛到的形状」。**不是。** WASIp2 是:
80101

81102
```
82-
"tcp://10.0.0.1:80" ──namespace──▶ 一个 stream
83-
"/etc/hosts" ──namespace──▶ 一个 stream
84-
UART ──BSP 接线──▶ stdout(也是 stream)
103+
wasi:io/streams input-stream / output-stream ← 共享的传输资源
104+
wasi:filesystem/types descriptor ← 自有资源类型,能产出 stream
105+
wasi:sockets/tcp tcp-socket ← 自有资源类型,能产出 stream
85106
```
86107

87-
**TCP 连接、文件、管道、串口给你的是同一种资源,只是命名方式不同。**
88-
这比 POSIX 的 "everything is a file"(把命名和资源混成一个整数)干净:
89-
**命名失败和读写失败是两件事,现在它们在两个接口里。**
108+
它把**资源种类分开**,共享的是 **stream 这个传输类型**。草案把「共享 stream」
109+
误读成了「统一命名」。
110+
111+
**保留对的那半(流统一了传输),丢掉错的那半(统一命名)。**
112+
113+
| | namespace 草案 | 撤回后 | 单体 fs+net |
114+
|---|---|---|---|
115+
| 实现者 | ⛔ 人人要 URI 解析器 | ✅ 没有就**不提供**该 interface | ✅ 同 |
116+
| 消费者 | ⛔ 错误是字符串;**编译期不知道支不支持** |`import openkal.net;` 缺了就**编译期报错** | ✅ 同 |
117+
| 规范负担 | ⛔ 要标准化 **scheme 注册表** = 巨大隐藏面 | ✅ 每 interface 独立版本,面有界 | ⚠️ 接口大但有界 |
118+
| 类型安全 | ⛔ caps 成为不相干能力并集 | ✅ 文件操作在文件句柄上 | ✅ 同 |
119+
120+
⚠️ **划分原则(按资源种类)没错,错的是塌缩过头** —— `descriptor` / `socket` /
121+
`stream` 本来就是三种资源。
90122

91-
**net 不是设备,但网卡是** —— 按基数分(§10):网卡 N 个 → openhal;协议栈 1 个 → openkal。
123+
**net 不是设备,但网卡是** —— 按基数分(§12):网卡 N 个 → openhal;协议栈 1 个 → openkal。
92124

93125
---
94126

@@ -159,11 +191,14 @@ if constexpr (requires { kal::seek(s, 0); }) // ⚠️ 名字不存在 ⇒ 硬
159191
|---|---|---|---|
160192
| **① 接口在不在** | 有没有 `openkal.task` | **模块导入** | 编译期,点名模块 |
161193
| **② 接口内的操作在不在** |`write``seek` | **constexpr caps** | 编译期,`static_assert` 文案 |
162-
| **③ 语义能力** | 有没有 MMU、能否阻塞 | **`cfg()`** | 依赖解析期 |
194+
| **③ 语义能力** | 抢占式 vs 协作式调度 | **`cfg()`** | 依赖解析期 |
163195

164196
⚠️ **③ 绝不能做成 concept**:ⓘ K1/K2 实测 `RiscvSv39``NoMmu` **同时满足**同一个
165197
`AddressSpace` concept,通用代码在 NoMmu 上**静默失败****concept 检查语法,不检查语义。**
166198

199+
**但 openkal core 里一条 ③ 都没有** —— 见 §4.3。K1/K2 那个例子是 **openarch**
200+
`AddressSpace`,不是 openkal 的。
201+
167202
### 4.1 ①:让模块解析本身成为能力检查
168203

169204
```
@@ -178,8 +213,31 @@ import openkal.stream.caps; // 没有后端 ⇒ 编译期找不到模块,点
178213

179214
**「没有实现者」不是链接器吐未定义符号,而是编译器说模块不存在。**
180215

181-
mcpp 侧承载件已有:后端由 **`cfg()` 条件依赖**选,接口包用 **`reexport = true`**
182-
把后端 provisions 透给消费者 —— 对应「后端选择 = 条件依赖,零新增轴」。
216+
⚠️ **接法要注意方向。** 草案曾写「接口包 `reexport` 后端的 provisions」——
217+
**错的**:ⓘ `reexport`**向下游**传播(`grpc` 把 protoc 透给它的用户),
218+
而这里需要的是接口拿到**消费者所选后端**提供的东西,方向相反,`reexport` 表达不了。
219+
220+
正确接法是**反过来**,而且正好是 `reexport` 的本意:
221+
222+
```toml
223+
# 后端包 openkal-uart 的 manifest
224+
[dependencies]
225+
openkal-stream = { version = "0.1", reexport = true } # 把接口透给我的消费者
226+
```
227+
228+
```toml
229+
# 消费者:只写后端,按 target 选;接口随之而来
230+
[target.'cfg(os = "linux")'.dependencies]
231+
openkal-linux = "0.1"
232+
[target.'cfg(os = "none")'.dependencies]
233+
openkal-uart = "0.1"
234+
```
235+
236+
源码 `import openkal.stream;` 两个 target 一字不改。
237+
**零新增引擎轴,且用的是已有机制的本意。**
238+
239+
⚠️ 两个后端同时进图会造成 `openkal.stream.caps` 模块重复定义 —— 基数为 1(§12)
240+
使这成为用户错误,mcpp 会报模块冲突。
183241

184242
### 4.2 ②:能力是****
185243

@@ -199,8 +257,8 @@ struct caps {
199257
if constexpr (kal::stream_caps::caps::seek) { kal::seek(s, off); }
200258
201259
static_assert(kal::stream_caps::caps::seek,
202-
"this backend has no seekable streams; openkal.namespace is the "
203-
"interface that hands out seekable ones");
260+
"this backend has no seekable streams; openkal.fs hands out "
261+
"descriptors that do");
204262
```
205263

206264
组合 = **concept over caps**,不是 concept over 符号:
@@ -210,15 +268,27 @@ template <class C> concept Sequential = C::sequential;
210268
template <class C> concept Seekable = Sequential<C> && C::seek;
211269
```
212270

213-
### 4.3 ③:`cfg()`—— ⚠️ 今天文法不够
271+
### 4.3 ③:⚠️ openkal core 用不到它 —— 一条被撤回的引擎改动
214272

215-
mcpp 今天 `cfg()` 的文法是 **os / arch / family / env + all/any/not**,**没有能力谓词**
216-
`cfg(mmu)` 这类需要**扩文法**
273+
草案曾要求扩 `cfg()` 文法以支持 `cfg(mmu)` 这类能力谓词。**重估后撤回。**
217274

218-
⚠️ 这是本方案**唯一需要引擎改动**的一处,而且与「零新增轴」不冲突 ——
219-
是给已有的 `cfg` 加一个词类,不是加一条新轴。
275+
那条结论的出处是 K1/K2,而 K1/K2 测的是 **openarch 的 `AddressSpace`** —— 草案把它
276+
搬进了 openkal。逐个接口检查 openkal 有没有「存在但语义不同」的能力:
220277

221-
---
278+
| interface | 有 ③ 类语义轴吗 |
279+
|---|---|
280+
| `abort` ||
281+
| `stream` | seek / nonblock / vectored 都是**操作**(②类) |
282+
| `memory` | 静态 arena vs 按需分页 = **容量**不是能力;分配失败到处都有定义 |
283+
| `time` | monotonic vs wall 是**两种资源**,不是一个资源的两种语义 |
284+
| `task` | ⚠️ 抢占 vs 协作**确实是** —— 但那是 D1 以后的事 |
285+
286+
**core(abort + stream + memory)一条语义轴都不需要,①② 足够。**
287+
288+
而且 **triple 本身已经承载了大部分**:`riscv64-none-elf``riscv64-linux-gnu`
289+
区别里就包含了 MMU 用不用。今天已有的文法足以选后端。
290+
291+
**撤回后,本方案变成零引擎改动。**
222292

223293
## 5. ⭐ caps 撒谎问题:四层防御,按强度排
224294

@@ -600,10 +670,113 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零
600670
601671
| | 为什么 |
602672
|---|---|
603-
| **`open(path)` 进 core** | WASIp1 的教训;命名进 `openkal.namespace`,且不是 core |
673+
| **`open(path)` 进 core** | WASIp1 的教训;开文件在 `openkal.fs`(自有句柄类型),不在 core,也不经一个通用命名接口(§2.3) |
604674
| **运行期 `ENOSYS`** | 部分性用「链接哪些 interface」表达;⭐ gcc 的 `gthr-single.h` 已是三十年的先例 |
605675
| **errno 透传** | 封闭 `enum class`;映射不是模拟 |
606676
| **虚接口 / vtable** | ABI 脆弱,与「只增不改」冲突 |
607677
| **`thread_local` 的保证** | 属于 openarch + BSP(§10.4) |
608678
| **引擎认识 openkal** | ⚠️ 计划 §7.2 第一行:**引擎永不认识这三层**;后端选择 = 条件依赖,零新增轴 |
609679
| **现在就实现** | D0 的门是「第三方来了没有」,不是「能不能写出来」 |
680+
681+
---
682+
683+
## 15. 撤回记录
684+
685+
⚠️ 草案里被 review 推翻的两条,连同它们**为什么当时看起来对**:
686+
687+
| 撤回的 | 当时的理由 | 为什么错 |
688+
|---|---|---|
689+
| **`openkal.namespace`**(取代 fs/net) | 「文件 / socket / UART 给你的都是 stream,只是命名方式不同」 | ① 触犯本文自己的 §5.1 规矩(caps 成为不相干能力并集);② URI 解析器**就是**模拟层,违反对下判据;③ 引用的 WASIp2 先例是**误读**(它分开资源种类,只共享 stream 类型) |
690+
| **扩 `cfg()` 文法支持 `cfg(mmu)`** | K1/K2 说「MMU 是能力轴不是契约」 | 那条结论是关于 **openarch** 的;openkal core 逐个接口查下来**一条 ③ 类语义轴都没有**,而且 triple 本身已承载大部分 |
691+
| **「接口包 `reexport` 后端」** | 以为 mcpp 的承载件现成 | ⓘ `reexport` 是**向下游**传播,方向相反。正确接法是**后端 reexport 接口**(§4.1),恰好是该机制的本意 |
692+
693+
⭐ 三条的共同形状:**都是「我有一个漂亮的统一」,而漂亮的统一把两件本来不同的事合并了。**
694+
这与 §5.1 给出的判据是同一条 —— 只是那一节我用它去检查别人的后端,没有用它检查自己的分解。
695+
696+
---
697+
698+
## 16. 综合 review 发现的开放问题
699+
700+
撤回两条之后重新通读,又找出六条 —— 都是**规范必须表态、但草案没表态**的。
701+
702+
### 16.1 ⚠️ 两个堆(ⓘ 实测,不是理论)
703+
704+
ⓘ picolibc `libc.a` 的 `vfprintf.c.o` **引用 `free`** —— **printf 与分配器是耦合的**。
705+
而固件里已有一整套 `malloc`/`free`/`__malloc_sbrk_aligned`/`__fallback_sbrk`。
706+
707+
⇒ 若 `operator new` 走 `kal_alloc`(openkal 的 arena)而 `printf` 走 picolibc 的
708+
`malloc`,**同一块 RAM 上会有两个分配器**,而且都想长 sbrk。
709+
710+
**规范必须写死一条**:
711+
712+
> **后端上若已存在 libc 分配器,`kal_alloc` 必须实现在它之上,而不是与它并列。**
713+
714+
三种配置:
715+
716+
| 后端 | 做法 | |
717+
|---|---|---|
718+
| picolibc | `kal_alloc` → `malloc`(openkal 在 libc **之上**) | ✅ 一个堆 |
719+
| 零 libc | `kal_alloc` → 自带 arena;没有 libc malloc | ✅ 一个堆 |
720+
| ⛔ 自带 arena **且** libc malloc 也在 | — | **禁止** |
721+
722+
⚠️ 这条部分可 conformance 化:检查固件里 `sbrk` 的消费者是不是只有一个。
723+
724+
### 16.2 ⚠️ 错误集合的封闭性
725+
726+
草案说「封闭 `enum class`,不透传 errno」。但 POSIX 有约 130 个 errno,
727+
一个 ~15 项的封闭集合**必然丢信息**。规范要表态:
728+
729+
- 丢掉细节(简单,但诊断质量下降),还是
730+
- 留一个 `other` + **后端私有的细节通道**(`kal_last_error_detail()`),
731+
⚠️ 但那是全局状态,和 errno 一样的毛病
732+
733+
**倾向**:封闭集合 + 细节通道**只用于日志**,不进控制流。需要写进 SPEC。
734+
735+
### 16.3 ⚠️ C ABI 没有版本,而结构体布局会被永久冻结
736+
737+
「每个 interface 独立版本 + 只增不改」保护得了**函数**(加新函数是可加的),
738+
保护不了**结构体**:`kal_io_result` 的布局一旦发布就永远不能动。
739+
740+
选项:符号带版本后缀(`kal_stream_write_v1`,难看但诚实),或**明确声明这些布局永久冻结**。
741+
草案默认了后者却没写出来。
742+
743+
### 16.4 ⚠️ 拥有 vs 借用,C ABI 强制不了
744+
745+
标准流是**借用**(不配 `close`),`openkal.fs` 的 descriptor 是**拥有**(必须 close)。
746+
C ABI 里没有 RAII,谁来保证?
747+
748+
⇒ 类型化 C++ 层可以包 RAII,但**那不是规范的一部分**,Rust/C 消费者拿不到。
749+
规范至少要把「哪些句柄是拥有的」写清楚,并规定重复 close 的行为。
750+
751+
### 16.5 ⚠️ 线程安全未规定
752+
753+
同一个 `kal_stream` 被两个 task 同时 `write`,是什么行为?
754+
POSIX 至少规定了 `PIPE_BUF` 以内的原子性。**草案一个字没提。**
755+
这条在有 `openkal.task` 之后立刻变成必须回答的。
756+
757+
### 16.6 ⚠️ sized-free 的方向性成本
758+
759+
`kal_free(p, size, align)` 对 arena 友好(Rust 的做法),但:
760+
761+
- hosted 后端 `kal_free` → `free(p)`,**丢掉 size**:无成本 ✅
762+
- 反向:一个建在 `kal_alloc` 之上的 libc `malloc` **必须自己存 size** ⇒ 每次分配多一个字
763+
764+
⚠️ 与 §16.1 的规则合看,反向配置本来就该避免,所以成本可控 —— 但要写明。
765+
766+
---
767+
768+
## 17. 这一轮 review 的元结论
769+
770+
三条撤回(§15)+ 六条开放问题(§16)里,有一个共同形状值得单独记:
771+
772+
⭐ **草案的错误全部是「一个漂亮的统一,把两件本来不同的事合并了」**:
773+
774+
- `openkal.namespace` 合并了「命名」与「资源种类」
775+
- `cfg(mmu)` 把 openarch 的结论搬进 openkal
776+
- 「两个堆」是没有合并该合并的(分配器)
777+
778+
而 §5.1 给出的判据 —— **「一个操作若能『存在但永远失败』,说明它被错误地合并了」** ——
779+
本来就能抓住前两条。
780+
781+
⚠️ **我只用它去检查别人的后端,没有用它检查自己的分解。**
782+
⇒ 判据要对**自己的设计**先跑一遍,再拿去当准入门槛。

0 commit comments

Comments
 (0)