Skip to content

Commit 5f5c831

Browse files
committed
docs(openkal): add a complete Linux reference implementation
Two identities: a backend that works today, and the thing other implementers copy. It does not move the D0 gate — that gate is whether a THIRD PARTY writes a third backend — but it turns 'guess the shape and write the implementation' into 'write the implementation'. Writing it out surfaced three things the design document had not: * core operations need no ADL at all. They are declared `extern "C"` by the interface and defined by the backend; missing means a link error. The ADL mechanism serves optional capabilities only, which makes the common path simpler than the draft implied. * short writes are a spec question nobody had asked. ::write(2) may write less than requested, so openkal must choose: write-all-or-error (the loop lives once, in the backend) or allow short writes (every caller writes the loop — which is exactly where POSIX has tripped programs up for decades). * ⭐ it independently confirms the fs/net decomposition. On Linux, seekability is a property of the HANDLE, not of the backend — lseek succeeds on a file and returns ESPIPE on a pipe. If openkal.stream had seek, the Linux backend could not answer honestly: claiming it means always failing on pipes, which is precisely the 'present but useless' antipattern. Because §2.3 puts seek on openkal.fs's descriptor instead, the question does not arise. That last point is the strongest argument for writing a complete reference at all: it is the only way to find a decomposition error, and it finds it earlier than a conformance suite would.
1 parent 8bd786f commit 5f5c831

1 file changed

Lines changed: 190 additions & 1 deletion

File tree

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

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,17 @@ thread_local int counter; → 编译 ✅ 链接 ✅ 零未定义符号 ✅ 零
837837
838838
⚠️ 这条部分可 conformance 化:检查固件里 `sbrk` 的消费者是不是只有一个。
839839
840+
### 16.1b ⚠️ 短写:`kal_stream_write` 写不全时怎么办(§20.3 逼出来的)
841+
842+
写参考实现时立刻撞上:`::write(2)` 可以短写。SPEC 必须选一边:
843+
844+
| | 后果 |
845+
|---|---|
846+
| **写全或报错**(推荐) | 循环在**后端**里写一次 |
847+
| 允许短写 | ⚠️ **每个调用方**都要自己写循环 —— 这正是 POSIX 让无数程序出错的地方 |
848+
849+
⇒ 建议规定「写全或报错」,短写只在 `kal_stream_write_some`(若真需要)里出现。
850+
840851
### 16.2 ⚠️ 错误集合的封闭性
841852
842853
草案说「封闭 `enum class`,不透传 errno」。但 POSIX 有约 130 个 errno,
@@ -964,4 +975,182 @@ unsupported_t seek(S, long) { … }
964975
| **引擎改动** | ✅ **零** |
965976
| **对下/对上判据** | ✅ 双向且可数(数桥接行数) |
966977
| ⚠️ **开放问题** | §16 六条 + §18 两条,**全部需要 SPEC 表态** |
967-
| ⚠️ **最大风险** | 不是技术 —— 是 **D0 的门:有没有第三方来实现第三个后端** |
978+
| ⚠️ **最大风险** | 不是技术 —— 是 **D0 的门:有没有第三方来实现第三个后端**。⭐ 缓解手段是 §20 的官方 `openkal-linux` 完整参考实现:**降低门槛,但不移动门** |
979+
980+
---
981+
982+
## 20. 官方参考实现:`openkal-linux`(完整)
983+
984+
⭐ **它有两个身份**:一个**当天可用**的后端,和**其它实现者要抄的那份样板**。
985+
986+
⚠️ 它**不能移动 D0 的门**(门是「有第三方实现了第三个后端」),但它把门**变得够得着** ——
987+
没有可抄的样板时,第三方要同时猜形状和写实现。
988+
989+
### 20.1 ⭐ 写这份实现时才发现的一条:core 操作根本不需要 ADL
990+
991+
| | 声明在哪 | 后端提供什么 | 探测机制 |
992+
|---|---|---|---|
993+
| **core 操作**(write/read/alloc/abort) | 接口包 `extern "C"` + C++ 封装 | **只有 C 函数的定义** | **不需要** —— core 的定义就是「一定在」 |
994+
| **可选能力**(seek/vectored/…) | 后端声明 C++ 重载 | 声明 + 定义 | **ADL**(§4.1) |
995+
996+
⇒ ADL 那套只服务**可选能力**。core 走最简单的路:接口声明,后端定义,缺了就是链接错误。
997+
998+
### 20.2 包结构
999+
1000+
```
1001+
openkal-linux/
1002+
├── mcpp.toml
1003+
└── src/
1004+
├── stream.cppm export module openkal.stream; ← 提供接口名
1005+
├── stream.cpp extern "C" 定义
1006+
├── memory.cppm export module openkal.memory;
1007+
├── memory.cpp
1008+
├── abort.cppm export module openkal.abort;
1009+
└── abort.cpp
1010+
```
1011+
1012+
```toml
1013+
[package]
1014+
name = "openkal-linux"
1015+
version = "0.1.0"
1016+
1017+
[dependencies]
1018+
openkal = "0.1" # 契约
1019+
1020+
[target.'cfg(not(linux))'.build]
1021+
# 这个后端只在 linux 上有意义;别的 target 上它不该被选中
1022+
```
1023+
1024+
### 20.3 `openkal.stream`
1025+
1026+
```cpp
1027+
// src/stream.cppm —— 提供应用可见的名字,自己不加任何非标准的东西
1028+
export module openkal.stream;
1029+
export import openkal.decl.stream;
1030+
// core 操作无需在此声明:它们是 openkal.decl.stream 里的 extern "C" + 封装。
1031+
// 这个后端也不提供 seek —— 见 20.6,那不是疏漏。
1032+
```
1033+
1034+
```cpp
1035+
// src/stream.cpp
1036+
#include <unistd.h>
1037+
#include <errno.h>
1038+
import openkal.decl.stream;
1039+
1040+
extern "C" {
1041+
1042+
kal_stream kal_stdin (void) { return kal_stream{0}; }
1043+
kal_stream kal_stdout(void) { return kal_stream{1}; }
1044+
kal_stream kal_stderr(void) { return kal_stream{2}; }
1045+
1046+
kal_io_result kal_stream_write(kal_stream s, const void* buf, uintptr_t n) {
1047+
auto* p = static_cast<const unsigned char*>(buf);
1048+
uintptr_t done = 0;
1049+
while (done < n) {
1050+
ssize_t r = ::write(static_cast<int>(s.h), p + done, n - done);
1051+
if (r < 0) {
1052+
// ⚠️ EINTR 必须重试。漏掉它的后端在有信号的系统上会随机短写,
1053+
// 而这类 bug 在测试里几乎不出现。
1054+
if (errno == EINTR) continue;
1055+
return { done, kal_from_errno(errno) };
1056+
}
1057+
if (r == 0) break;
1058+
done += static_cast<uintptr_t>(r);
1059+
}
1060+
return { done, 0 };
1061+
}
1062+
1063+
kal_io_result kal_stream_read(kal_stream s, void* buf, uintptr_t n) {
1064+
for (;;) {
1065+
ssize_t r = ::read(static_cast<int>(s.h), buf, n);
1066+
if (r < 0) { if (errno == EINTR) continue; return { 0, kal_from_errno(errno) }; }
1067+
return { static_cast<uintptr_t>(r), 0 }; // 短读是正常的,不重试
1068+
}
1069+
}
1070+
1071+
int32_t kal_stream_flush(kal_stream) { return 0; } // 裸 fd 无用户态缓冲
1072+
1073+
}
1074+
```
1075+
1076+
⚠️ **写这段逼出了一个 SPEC 必须回答的问题**(§16 没覆盖):
1077+
1078+
> `kal_stream_write` 返回**短写**,还是**写全或报错**?
1079+
1080+
上面选了「循环到写全」。若 SPEC 选另一边,**每个调用方都要自己写这个循环** ——
1081+
这正是 POSIX 让无数程序出错的地方。⇒ **建议 SPEC 规定「写全或报错」,短写只在
1082+
`kal_stream_write_some`(如果需要)里出现。**
1083+
1084+
### 20.4 `openkal.memory` —— 演示 §16.1 的规则
1085+
1086+
```cpp
1087+
// src/memory.cpp
1088+
#include <stdlib.h>
1089+
1090+
extern "C" {
1091+
// ⭐ 建在 libc 分配器之上,不是与它并列 —— §16.1 的规则,这里是它的正面示例。
1092+
void* kal_alloc(uintptr_t size, uintptr_t align) {
1093+
if (align <= alignof(max_align_t)) return ::malloc(size);
1094+
return ::aligned_alloc(align, (size + align - 1) / align * align);
1095+
}
1096+
// sized-free:这个方向丢掉 size 是零成本的(§16.6)
1097+
void kal_free(void* p, uintptr_t, uintptr_t) { ::free(p); }
1098+
}
1099+
```
1100+
1101+
### 20.5 `openkal.abort`
1102+
1103+
```cpp
1104+
// src/abort.cpp
1105+
#include <unistd.h>
1106+
#include <stdlib.h>
1107+
1108+
extern "C" {
1109+
[[noreturn]] void kal_abort(const char* msg, uintptr_t len) {
1110+
if (msg && len) { ssize_t r = ::write(2, msg, len); (void)r; }
1111+
::abort();
1112+
}
1113+
[[noreturn]] void kal_exit(int32_t code) { ::_exit(code); }
1114+
}
1115+
```
1116+
1117+
⚠️ `_exit` 而不是 `exit`:`exit` 会跑 atexit 与静态析构,而 `kal_exit` 的契约是
1118+
「立刻结束」。这类差别**必须写进 SPEC**,否则两个后端的语义会悄悄分叉。
1119+
1120+
### 20.6 ⭐ 参考实现验证了 fs/net 的分解(§2.3)
1121+
1122+
写 Linux 后端时会立刻撞上一件事:
1123+
1124+
> **Linux 上「能不能 seek」是每个句柄的属性,不是后端的属性。**
1125+
> `lseek(fd)` 对普通文件成功,对管道 `ESPIPE`。
1126+
1127+
⇒ 如果 `openkal.stream` 有 `seek`,Linux 后端**无法诚实回答** ——
1128+
声称有,则对管道永远失败(**正是 §5.1 的「存在但永远失败」反模式**);
1129+
声称没有,则文件用不了。
1130+
1131+
⭐ **而 §2.3 的分解让这个问题不存在**:seek 属于 `openkal.fs` 的 descriptor 类型,
1132+
`openkal.stream` 压根没有它。**参考实现独立地证实了那次撤回是对的。**
1133+
1134+
⇒ **这是「写一份完整实现」最大的价值:它是唯一能发现分解错误的方法,
1135+
而且比 conformance 更早。**
1136+
1137+
### 20.7 它作为样板教什么
1138+
1139+
| 样板里的模式 | 其它实现者照抄什么 |
1140+
|---|---|
1141+
| `stream.cppm` 只有 `export import`,不加任何东西 | **不要往标准模块名里塞私货**(§4.4) |
1142+
| EINTR 循环 | 每个后端都要处理自己平台的「被打断」 |
1143+
| `kal_alloc` 走 `malloc` | §16.1:有 libc 分配器就建在它之上 |
1144+
| `kal_from_errno` 是一张**表** | **映射 ≠ 模拟**(§3.1) |
1145+
| 没有 `seek` | 能力缺失就是**不声明**,不是声明后返回错误 |
1146+
| `_exit` 而非 `exit` | 语义细节要向 SPEC 对齐,不要凭直觉 |
1147+
1148+
### 20.8 对 D0 的影响:降低门槛,不移动门
1149+
1150+
| | |
1151+
|---|---|
1152+
| D0 的门 | **有第三方实现了第三个后端** —— 不变 |
1153+
| 官方 linux 后端做的事 | 把「猜形状 + 写实现」减成**只写实现** |
1154+
| ⚠️ 不做的事 | 它**不算**第三方后端,也不算第三个后端(linux/bare 是官方的两个) |
1155+
1156+
⇒ 判据仍然是**别人来不来**,而这份实现让「来」这件事从一个季度变成一个周末。

0 commit comments

Comments
 (0)