diff --git a/.agents/docs/2026-08-30-spawn-is-one-operation-not-a-family.md b/.agents/docs/2026-08-30-spawn-is-one-operation-not-a-family.md new file mode 100644 index 0000000..381505e --- /dev/null +++ b/.agents/docs/2026-08-30-spawn-is-one-operation-not-a-family.md @@ -0,0 +1,184 @@ +# openkal 0.11:把 spawn 家族收敛成一个操作 + +> 前提变了:**openkal 还没有外部用户**,处在前期设计与内部验证阶段。所以 +> clause 8「不许改已有声明」这条**在 0.x 期间不作为约束**——可以删。 +> +> 这一条把结论整个翻过来了。原提案是「加一个通用形式,忍受三个冗余声明」; +> 既然能删,就**没有理由留历史拼法**。 + +## 1. 现状:同一个操作,三个拼法 + +| 声明 | 多出来的那件事 | +|---|---| +| `kal_process_spawn` | 基本形 | +| `kal_process_spawn_with` | 交给它一组目录(preopen) | +| `kal_process_spawn_bound` | 生命周期绑在调用者身上 | + +这不是三个操作,是**同一个操作的三个修饰**。之所以成了三个声明,唯一的原因是 +clause 8 不许给已有声明加参数。 + +`process.h` 自己写下了这条路的尽头: + +> Declaring every combination is how an interface acquires four spawns and then +> eight, so the combination is declared when something needs it and not before. + +⚠️ **而「something needs it」已经到了,证据是一个函数签名:** + +``` +packages/kaos/src/process.cppm:87 + run_shell_stream(argv, cwd, …, timeout_ms) +:112 setpgid(0, 0); ← 进程树终止 +:113 if (!cwd.empty()) chdir(cwd.c_str()); ← 工作目录 +``` + +相邻两行,同一个子进程。四个修饰互相正交,全组合是 2⁴ = 16,而且**今天就需要 +其中一个组合**。 + +## 2. 设计 + +```c +/* 一次启动与另一次启动之间会变的东西,全在这里 —— 于是这是一个操作,不是一族。 */ +struct kal_spawn { + struct kal_dir base; /* `path' 相对谁解析 */ + struct kal_dir work; /* 程序在哪个目录里跑 */ + struct kal_job* job; /* 进/出:所属单位。见 2.4 */ + const struct kal_preopen* grants; /* 交给它的目录;可为空,计数为零 */ + kal_uintptr grant_count; + kal_uintptr flags; /* KAL_SPAWN_BOUND_LIFETIME */ +}; + +int kal_process_spawn(const struct kal_spawn*, + const char* path, kal_uintptr path_len, + const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, + const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, + const struct kal_spawn_streams* streams, + struct kal_process* out); +``` + +### 2.4 ⚠️⚠️ 这一节改过一次,而改它的是 clause 7.1 + +上面 `job` 那个字段,**第一版是一个位** `KAL_SPAWN_OWN_JOB`:让被起的程序自成一个 +单位,然后由 `kal_process_terminate` 事后够到它。两个实现零状态就能满足—— +`getpgid(pid) == pid` 能从内核把关联**恢复**出来;第三个不行:它**能组建**单位 +(job object 就是),但**无法从进程句柄恢复**一个,而 `kal_process` 只有一个机器字 +且已经装着进程。 + +⇒ 满足它就得维护一张登记表,而 clause 7.1 把这件事的含义写成了机械判据: +「an implementation that must maintain a translation table, a registry, or a name +resolver … indicates that **the specification has taken a shape borrowed from one +environment, and the shape is at fault**」。⭐ **所以错的是那个位,不是那个实现。** + +⚠️ **而显而易见的修法只会把缺陷搬个方向。** 一个「打开一个空单位」的操作,在 +「先建单位再塞成员」的系统上自然,在「单位由第一个成员创建」的系统上**不可能** +——进程组的标识就是某个进程的标识,没有成员就没有组可开。那是同一条 clause 7.1 +判据换个方向拼写。 + +⇒ 最终形状:**身份在第一个成员启动时确立并回报给调用者**。`job` 为空 = +不参与任何单位;指向零 = 新建一个,把身份写回来;指向一个名字 = 加入它。 +两类系统都不需要记住任何东西。 + +```c +struct kal_job { kal_uintptr h; }; +int kal_process_job_enter(struct kal_job*); /* 把调用者自己放进一个单位 */ +int kal_process_job_terminate(struct kal_job); +void kal_process_job_close(struct kal_job); +``` + +⭐ **两个不是目标的收益**:`kal_process_terminate` 恢复成「永远只是一个程序」 +(位形式下它的含义取决于句柄上一个不可见的属性,那正是这个接口最反对的隐藏状态); +以及**多个程序可以共享一个单位**,位形式说不出这句话。 + +⚠️ 三处写进声明而不是留给人撞:释放一个单位**不得**结束它(这个系统的 job object +可以被要求那样做,而一旦那样,同一个操作在两个系统上就是两个意思);由进程标识命名 +的单位**比由句柄命名的弱**,因为标识会被回收;以及进入一个单位在某些系统上**要付 +代价**(脱离终端前台组),所以它必须是按次选的。 + +### 2.5 ⭐ 还加了一条:`kal_process_stop_requested` + +```c +const kal_u32* kal_process_stop_requested(void); /* 零 → 非零,只增不清 */ +``` + +「信号」不是一个能力,是一个装了八种用途的筐,而这个接口**已经表达了其中七种**: +终止一个程序、终止一个单位、流的对端没了(`kal_stream_write` 报错)、被起的程序 +结束了(`kal_process_wait`)、等待的期限(`openkal.timeout`)、唤醒另一个上下文 +(`kal_task_wake`)。第八种没有任何拼法——**本程序被请求结束**——而 shell、服务器、 +带超时的 runner 都是围着它写的。 + +⚠️ **而信号本身不能成为接口**:它是一个环境的形状(编号集合、disposition、打断当前 +栈的 handler)。另一个系统的通知**在它自己起的上下文上到达**,覆盖的原因也不同; +两个目标根本没有进程可以被 signal。clause 7.1 说的正是这种形状。 + +⇒ 一个字,而不是一个 handler。读它,或者用 `kal_task_wait` **等**它——这个规范本来 +就有,而 `openkal.timeout` 本来就给它加了期限。**零个新概念**,也没有 handler 强加给 +每个调用者的那套再入规则。 + +⚠️ 它是**通知不是否决**;它**不说是谁请求的**;它对**无法被观察的终结**(不可拒绝的 +信号、突然终止、`kal_process_job_terminate`)**什么都不说**——因为那些在任何系统上 +都不产生通知。⭐ 正是这个对称性让它在两类系统上都可实现,而不是只在一类上。 + +**删掉** `kal_process_spawn_with` 与 `kal_process_spawn_bound`。 + +⭐ **名字用回 `kal_process_spawn`,不叫 `kal_process_start`。** 能删就没有理由让 +接口上同时存在「旧的 spawn」和「新的 start」——一个意思一种拼法。旧调用点会**编译 +失败**(参数个数与类型都不同),这是响亮的失败而不是安静的错行为。 + +### 2.1 ⭐ `work` 是必填的,而这是设计而不是负担 + +openkal **故意没有环境级的当前目录**——「我碰巧在的那个地方」在这个接口里不是一个 +可以指称的东西。所以「程序在哪儿跑」只能由调用者每次说明。 + +⇒ 一个不在乎的调用者传 `base`。**没有「不说」这个选项**,因为没有一个默认值是真的。 + +⚠️ 这正好补上 openkal 一直拒绝的那件事的另一半:拒绝「改一个运行中程序的工作目录」 +是对的(那是上下文之间的共享可变状态),但拒绝的理由**从来不适用于**「在启动的那一 +刻说明它在哪儿跑」——那是每次各说一次的、不可变的、不被共享的。 + +### 2.2 为什么 `OWN_JOB` 必须是按次选的开关 + +⚠️ 新进程组会**脱离终端前台组**,带界面的程序里子上下文读终端会拿到 SIGTTIN 停住。 +我在 0.10 评估时就是因为这个排除了「让 terminate 一律杀进程组」——**那个理由现在 +仍然成立**。三个流全是管道的调用者(跑 shell 的)要这个行为;交互式的绝不要。 + +⇒ 做成默认行为是错的,做成实现属性也是错的。**只有按次表达才对。** + +### 2.3 能力词 + +`kal_process_props` 保留 `KAL_PROCESS_PROP_GRANT_DIR`、`KAL_PROCESS_PROP_BOUND_LIFETIME`, +新增 `KAL_PROCESS_PROP_OWN_JOB`。 + +⭐ **`work` 不需要能力位**:三个提供 `openkal.process` 的实现都能做到——Linux/macOS +在替换前 `fchdir`,Windows 用 `CreateProcess` 的 `lpCurrentDirectory`。做不到的 +(opensbi/uefi)根本不提供这个接口。 + +⇒ `KAL_SPAWN_*` 说「调用者要什么」,`KAL_PROCESS_PROP_*` 说「实现能什么」,两个词 +不混。 + +## 3. 全局复核:同类毛病只有这一处 + +把 `SURFACE.txt` 里所有后缀族都过了一遍: + +| 族 | 判定 | +|---|---| +| `kal_env_var` / `_at` | 按名字取 vs 按下标枚举 —— 两个操作 | +| `kal_fs_close_dir` / `_file` | 两种资源类型,签名不同 | +| `kal_fs_open` / `_dir` | 返回 `kal_file` vs `kal_dir` | +| `kal_fs_set_modified` / `_at` | 持有打开的文件 vs 持有名字,等同 `fchmod`/`chmod` | +| **`kal_process_spawn` ×3** | **同一操作的三个修饰 —— 只有这一处** | + +⇒ 这次收敛的范围就是 spawn,不扩大。 + +## 4. 落地顺序 + +1. **openkal**:`struct kal_spawn` + 合并后的 `kal_process_spawn`,删两个声明; + SPEC clause 11 记录**删除本身**(为什么删、删了什么);SURFACE 减二; + `src/process.cppm`;conformance 的 process 节 +2. **openkal-linux / macos / windows**:新签名 + `work` + `OWN_JOB` +3. **openkal-opensbi / uefi**:只重钉版本 +4. **openkal-musl**:`okm_spawn.c` 换签名,`posix_spawn` 的 `addchdir` 接 `work`、 + `setpgroup` 接 `OWN_JOB` +5. **openkal-llvm-runtime / std-freestanding-alloc-kal**:重钉 +6. 用报告者的工程验证:kaos 三条红判据 + agent-core 三条,应当全绿 + +⚠️ **索引仍然要两批**,和 0.10 一样:alloc-kal 的 CI 从已发布的索引产物解析 +openkal,所以它必须等第一批落地、产物刷新之后才能绿。这是真实约束,不是疏漏。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f493848..c6bf23a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -292,6 +292,24 @@ jobs: done rm -rf .abi-probe + # ⚠️⚠️ EVERY MACRO HAS A MODULE SPELLING, AND THIS IS CHECKED RATHER THAN + # REMEMBERED --- because twice it was not. `KAL_LOCK_*' in 0.10 and + # `KAL_SPAWN_*' in 0.11 were each added to a header, used from a module + # consumer, and failed to compile there, because a macro does not cross a + # module boundary. Both were found by a consumer's build in another + # repository rather than here. + # + # ⇒ src/macros.cppm is GENERATED. Regenerating and diffing turns "somebody + # forgot" into a red build in the repository that made the change. + - name: Every macro has a module spelling + if: runner.os != 'Windows' + run: | + bash tools/gen-macros.sh + git diff --exit-code src/macros.cppm || { + echo "::error::src/macros.cppm is out of date --- run tools/gen-macros.sh and commit it" + exit 1 + } + - name: The module form compiles run: mcpp build diff --git a/SPEC.md b/SPEC.md index 4eee725..34bbd44 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.10 +# openkal Specification, version 0.11 ## 1. Scope @@ -50,7 +50,7 @@ provides an interface in whole or not at all. | `openkal.timeout` | a bound upon operations that would otherwise wait | optional | ✓ | ✓ | ✓ | | `openkal.event` | readiness of a set of resources | reserved | | | | -Version 0.10 specifies the core and optional interfaces. The reserved row is not +Version 0.11 specifies the core and optional interfaces. The reserved row is not specified, and its name shall not be used for other purposes. The S, L and X columns state which boundaries an interface's declarations can @@ -1057,7 +1057,8 @@ The following are recorded so that they are not mistaken for oversights. the line: a caller can build exclusion out of `KAL_OPEN_EXCLUSIVE` and a name, and nothing then releases that name when its holder dies. 11. **A started program that does not outlive its caller.** ⚠️ **Settled in - 0.10.** `kal_process_spawn_bound`. + 0.10** as `kal_process_spawn_bound`, **and respelled in 0.11** as + `KAL_SPAWN_BOUND_LIFETIME` — see entry 14. Clause 7.1 declines to replace a running image, and that stands. The consequence, which this entry did not previously record, is that a C library @@ -1102,3 +1103,76 @@ The following are recorded so that they are not mistaken for oversights. first look up. Bytes are what the caller is deciding about, so bytes are what this returns and the multiplication happens once, in the implementation, where the units are known. +14. **Starting a program was becoming a family, and 0.11 stopped it.** ⚠️ **This + is the first entry that records a REMOVAL**, and it is recorded here for the + same reason the additions are: so that the next reader meets the reasoning in + the specification rather than in a diff. + + 0.10 ended with three declarations — `kal_process_spawn`, + `kal_process_spawn_with` and `kal_process_spawn_bound` — which were never + three operations. They were one operation and three *modifiers* of it, + spelled apart for exactly one reason: clause 8 forbids adding an argument to + a declaration that exists, so each modifier had to arrive as a new name. + + ⭐ **The 0.10 text on `..._bound` predicted where that ends, in terms**: + "Declaring every combination is how an interface acquires four spawns and + then eight." Two more modifiers then arrived together — the directory a + program runs in, and terminating what a started program itself started — and + the evidence that they arrive *together* was a single consumer signature + whose child calls `setpgid(0, 0)` and `chdir(cwd)` on adjacent lines. Four + orthogonal modifiers is sixteen declarations, each written in every + implementation. + + ⇒ So the modifiers moved into `struct kal_spawn` and the family collapsed to + one declaration. **`..._with` and `..._bound` were removed rather than kept + beside it.** openkal has no users outside this ecosystem, so there was no one + to keep a second spelling for; an interface that offers two ways to say one + thing must explain the difference for ever, and this one had no difference to + explain. A caller written against the old shape fails to **compile** — + different arity, different types — which is the loud failure and not the + quiet one. + + ⚠️ **What this costs, stated rather than glossed.** `struct kal_spawn` is + frozen by clause 5.3, so a future modifier that carries a *parameter* rather + than a flag cannot be added to it and will need its own declaration after + all. This entry does not claim to have solved that; it claims that one + general form plus a flag word is a better place to meet the problem than + sixteen names, and that the modifiers wanted so far are all flags. + + ⚠️ **And clause 8 was set aside to do it**, deliberately and once. Clause 8 + exists to protect written code, and 0.11 is the last version at which there + is none to protect. An equivalent change after this specification has + consumers is not permitted by clause 8 and this entry is not a precedent for + one. +15. **`KAL_SPAWN_OWN_JOB` has a shape borrowed from one environment, and clause + 7.1 is what says so.** ⚠️ **Open, and recorded before it is settled** — because + the flag ships in 0.11 with one implementation refusing it, and a reader is + owed the reason. + + The flag asks that a started program and its descendants form one unit, and + `kal_process_terminate` end the unit. Two implementations satisfy it with no + stored state: a program that called `setpgid(0, 0)` has a group identifier + equal to its own, so `getpgid(pid) == pid` **recovers the association from + the kernel**. + + ⚠️ **openkal-windows cannot recover it.** It can form the unit — a job object + is exactly this — but there is no call there that answers "which job is this + process in" with a handle, and `kal_process` is one word already holding the + process. Satisfying the flag would require a registry keyed by process + handle. + + ⭐ **Clause 7.1 states the consequence mechanically**: an implementation that + must maintain a translation table, a registry, or a name resolver in order to + satisfy this specification indicates that the specification has taken a shape + borrowed from one environment, **and the shape is at fault rather than the + implementation**. So this entry is a defect of the flag, not of + openkal-windows, and openkal-windows refusing is the correct behaviour under + clause 6.2 meanwhile. + + ⇒ **The natural shape makes the unit a resource the caller holds**, created + before its members and terminated as a unit, so that nothing has to be + recovered on either kind of system. ⚠️ It is not settled here because the two + kinds of system create such a unit differently — Windows makes it externally + and assigns members, while a process group is made by a member from inside — + and a shape that is natural to one and not the other is the same defect + again, spelled the other way round. diff --git a/SURFACE.txt b/SURFACE.txt index 10048cb..f31ec17 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.10, one name per line. +# The C surface of openkal 0.11, one name per line. # # This file is normative and is the single source consulted by clause 9. A # conforming implementation exports the names of the interfaces it provides @@ -79,10 +79,12 @@ kal_fs_unlock kal_process_channel kal_process_channel_close kal_process_close +kal_process_job_close +kal_process_job_enter +kal_process_job_terminate kal_process_props +kal_process_stop_requested kal_process_spawn -kal_process_spawn_bound -kal_process_spawn_with kal_process_terminate kal_process_wait # openkal.task diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index 624d1c1..e97b70d 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -89,8 +89,10 @@ void okc_declarations_c(void) (void)sizeof(&kal_process_close); (void)sizeof(&kal_process_props); (void)sizeof(&kal_process_spawn); - (void)sizeof(&kal_process_spawn_bound); - (void)sizeof(&kal_process_spawn_with); + (void)sizeof(&kal_process_job_enter); + (void)sizeof(&kal_process_stop_requested); + (void)sizeof(&kal_process_job_terminate); + (void)sizeof(&kal_process_job_close); (void)sizeof(&kal_process_terminate); (void)sizeof(&kal_process_wait); (void)sizeof(&kal_random_fill); diff --git a/conformance/src/sections/child.cpp b/conformance/src/sections/child.cpp index aec8f58..f7a7cb9 100644 --- a/conformance/src/sections/child.cpp +++ b/conformance/src/sections/child.cpp @@ -218,7 +218,8 @@ bool start_copy_running(const char* first_element, const char* errand_argument, for (int i = 0; i < 2; ++i) { kal_uintptr n = 0; while (argv[i][n]) ++n; lens[i] = n; } const kal_spawn_streams streams{ 0, 0, 0 }; - return kal_process_spawn(base, rel, rel_len, argv, lens, 2, nullptr, nullptr, 0, + const kal_spawn how{ base, base, nullptr, nullptr, 0, 0 }; + return kal_process_spawn(&how, rel, rel_len, argv, lens, 2, nullptr, nullptr, 0, &streams, &out) == kal_ok; } diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 4694997..9da7b65 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -143,23 +143,59 @@ void run() { const kal_uintptr assigned = (kal::process::terminate | kal::process::stream_passing | kal::process::exit_status | kal::process::channel | kal::process::grant_dir - | kal::process::bound_lifetime).bits; + | kal::process::bound_lifetime + | kal::process::job + | kal::process::stop_requested).bits; observe(kind::abi, sizeof(kal_preopen) == 3 * sizeof(kal_uintptr), "a directory grant occupies three machine words"); + observe(kind::abi, sizeof(kal_spawn) == 6 * sizeof(kal_uintptr), + "the description of a start occupies six machine words"); + observe(kind::abi, sizeof(kal_job) == sizeof(kal_uintptr), + "a unit handle occupies one machine word"); observe(kind::abi, (kal_process_props() & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); - // ⚠️ AN OPERATION THAT IS NOT CLAIMED SHALL REFUSE RATHER THAN PERFORM + // ⚠️ A FLAG THAT IS NOT CLAIMED SHALL REFUSE RATHER THAN PERFORM // SOMETHING ELSE. A caller that asks for a bound lifetime asked for it; // a program started WITHOUT the binding is not the program it asked to // start, and an implementation that quietly starts one anyway is the - // failure the operation exists to remove. + // failure the flag exists to remove. + // + // ⭐ ONE LOOP OVER BOTH FLAGS RATHER THAN A BLOCK EACH, which is the + // shape 0.11 made possible: they are two positions in one word now, so + // the observation is written once and reads the same for the next flag + // that arrives. + // ⚠️⚠️ THIS SUITE OBSERVES THE REFUSAL AND NOT THE EFFECT, AND THAT IS A + // LIMIT OF WHAT openkal CAN SEE RATHER THAN AN OMISSION HERE. + // + // `work' sets the directory a started program runs in, and openkal has no + // operation that READS a working directory --- deliberately: it has no + // ambient one. `KAL_SPAWN_OWN_JOB' makes a started program and its + // descendants one unit, and openkal has no operation that enumerates + // processes. So a conforming implementation could honour both, or + // neither, and nothing written against openkal alone could tell. + // + // ⇒ They are in the same category as the streams a spawn is given: + // configuration of a program that openkal does not introspect, because a + // started program need not be an openkal program at all. The effects are + // observed where they can be --- openkal-musl's probes call `getcwd' and + // start a process tree --- and this suite observes what IT can, which is + // that an implementation not claiming a position refuses rather than + // starting a program that lacks what was asked for. + // + // ⚠️ THE MODULE SPELLINGS AND NOT THE MACROS. `KAL_SPAWN_*' are macros, + // and a macro does not cross a module boundary --- this suite consumes + // openkal as a module, so the C spelling is simply not in scope here. + // The same thing caught `KAL_LOCK_*' one release ago, which is why + // `kal::process::*_flag' exists at all. if ((kal_process_props() & kal::process::bound_lifetime.bits) == 0) { kal_process p{}; const char* argv[1] = { "x" }; const kal_uintptr lens[1] = { 1 }; - const int e = kal_process_spawn_bound(kal::fs::working(), "x", 1, argv, lens, 1, - nullptr, nullptr, 0, nullptr, &p); + const kal_spawn how{ kal::fs::working(), kal::fs::working(), nullptr, + nullptr, 0, kal::process::bound_lifetime_flag.bits }; + const int e = kal_process_spawn(&how, "x", 1, argv, lens, 1, + nullptr, nullptr, 0, nullptr, &p); observe(kind::behaviour, e == kal_err_not_supported, "a lifetime this implementation cannot bind is refused, not ignored"); } else { @@ -167,6 +203,28 @@ void run() { "a lifetime this implementation cannot bind is refused, not ignored", "the implementation claims prop_bound_lifetime"); } + + // ⭐ THE UNIT IS ASKED FOR BY A POINTER AND NOT BY A FLAG, so an + // implementation that cannot form one refuses a NON-NULL `job' --- there + // is no bit to set and none to test. + if ((kal_process_props() & kal::process::job.bits) == 0) { + kal_process p{}; + kal_job unit{}; + const char* argv[1] = { "x" }; + const kal_uintptr lens[1] = { 1 }; + const kal_spawn how{ kal::fs::working(), kal::fs::working(), &unit, + nullptr, 0, 0 }; + const int e = kal_process_spawn(&how, "x", 1, argv, lens, 1, + nullptr, nullptr, 0, nullptr, &p); + observe(kind::behaviour, e == kal_err_not_supported, + "a unit this implementation cannot form is refused, not ignored"); + observe(kind::behaviour, unit.h == 0, + "and the caller's unit handle is left as it was"); + } else { + unobserved(kind::behaviour, + "a unit this implementation cannot form is refused, not ignored", + "the implementation claims prop_job"); + } } if (performs(kind::stability)) { diff --git a/examples/portable/src/main.cpp b/examples/portable/src/main.cpp index 92a2fae..3dc94fa 100644 --- a/examples/portable/src/main.cpp +++ b/examples/portable/src/main.cpp @@ -249,10 +249,16 @@ int main() { if (!haveRoot) { say("openkal: not observed: no directory denoting the whole of the name space was supplied\n"); } else { + // `work' is the same directory as `base' here, which is what a + // caller that does not care about the working directory passes --- + // there is no "unset", because openkal has no ambient one that a + // default could mean. + const kal_spawn how{ root, root, nullptr, nullptr, 0, 0 }; + kal_process p{}; const char* argv[] = { "sh", "-c", "exit 0" }; const kal_uintptr lens[] = { 2, 2, 6 }; - bool spawned = kal_process_spawn(root, "bin/sh", 6, argv, lens, 3, + bool spawned = kal_process_spawn(&how, "bin/sh", 6, argv, lens, 3, nullptr, nullptr, 0, nullptr, &p) == kal_ok; int status = -1, terminated = 1; if (spawned) { kal_process_wait(p, &status, &terminated); kal_process_close(p); } @@ -261,7 +267,7 @@ int main() { // so that a status of zero is evidence rather than a default. kal_process q{}; const char* argv2[] = { "sh", "-c", "exit 3" }; - bool second = kal_process_spawn(root, "bin/sh", 6, argv2, lens, 3, + bool second = kal_process_spawn(&how, "bin/sh", 6, argv2, lens, 3, nullptr, nullptr, 0, nullptr, &q) == kal_ok; int status2 = -1, terminated2 = 1; if (second) { kal_process_wait(q, &status2, &terminated2); kal_process_close(q); } diff --git a/include/openkal/process.h b/include/openkal/process.h index ee39d43..f1e87f5 100644 --- a/include/openkal/process.h +++ b/include/openkal/process.h @@ -66,48 +66,160 @@ struct kal_preopen { kal_uintptr len; }; -/* Positions in kal_process_props. */ +/* Positions in kal_process_props --- what an IMPLEMENTATION can do. What a + * CALLER asks for is the KAL_SPAWN_ word below, and the two are deliberately + * separate: one describes the environment, the other one start. */ #define KAL_PROCESS_PROP_TERMINATE ((kal_uintptr)1u << 0) #define KAL_PROCESS_PROP_STREAM_PASSING ((kal_uintptr)1u << 1) #define KAL_PROCESS_PROP_EXIT_STATUS ((kal_uintptr)1u << 2) #define KAL_PROCESS_PROP_CHANNEL ((kal_uintptr)1u << 3) +/* A non-empty `grants' is answered here. */ #define KAL_PROCESS_PROP_GRANT_DIR ((kal_uintptr)1u << 4) -/* kal_process_spawn_bound is answered here. Version 0.10. */ +/* KAL_SPAWN_BOUND_LIFETIME is answered here. Version 0.10. */ #define KAL_PROCESS_PROP_BOUND_LIFETIME ((kal_uintptr)1u << 5) +/* A non-null `job' in kal_spawn is answered here. Version 0.11. */ +#define KAL_PROCESS_PROP_JOB ((kal_uintptr)1u << 6) +/* kal_process_stop_requested answers a word rather than null. Version 0.11. */ +#define KAL_PROCESS_PROP_STOP_REQUESTED ((kal_uintptr)1u << 7) + +/* ⚠️⚠️ HOW A PROGRAM IS STARTED, AND IT IS ONE OPERATION BECAUSE 0.11 STOPPED + * MAKING IT A FAMILY. + * + * Until 0.11 there were three declarations --- `kal_process_spawn', + * `..._with' and `..._bound' --- and they were never three operations. They were + * ONE operation and three modifiers of it, spelled as separate declarations for + * exactly one reason: clause 8 forbids adding an argument to a declaration that + * exists, so each new modifier had to arrive as a new name. + * + * ⭐ THE 0.10 COMMENT ON `..._bound' PREDICTED WHERE THAT ENDS, IN TERMS: + * "Declaring every combination is how an interface acquires four spawns and then + * eight, so the combination is declared when something needs it and not before." + * + * ⚠️ Something needed it, and the evidence was a single function signature in a + * consumer --- `run_shell_stream(argv, cwd, …, timeout_ms)', whose child calls + * `setpgid(0, 0)' and `chdir(cwd)' on ADJACENT LINES. Two more modifiers, wanted + * together. Four orthogonal modifiers is sixteen declarations, each of which must + * then be written in every implementation. + * + * ⇒ So the modifiers moved into a record and the family collapsed to its first + * member. `..._with' and `..._bound' are GONE rather than kept beside it: openkal + * has no users outside this ecosystem yet, so there is no one to keep a second + * spelling for, and an interface that offers two ways to say one thing has to + * explain the difference for ever. A caller written against the old shape fails + * to COMPILE --- different arity, different types --- which is the loud failure and + * not the quiet one. + * + * The layout is frozen (clause 5.3). */ +struct kal_spawn { + /* What `path' is resolved against. NAMING the program, and nothing else. */ + struct kal_dir base; + + /* ⭐ THE DIRECTORY THE PROGRAM RUNS IN, AND IT IS REQUIRED. + * + * openkal deliberately has no ambient working directory --- "wherever I happen + * to be" is not something this interface can name --- so there is no default + * that would be true, and a caller that does not care passes `base'. + * + * ⚠️ NAMING A PROGRAM AND NAMING WHERE IT RUNS ARE TWO DIRECTORIES. Before + * 0.11 there was one, and a C library above could not answer + * `posix_spawn_file_actions_addchdir_np' at all: `chdir' moved what the + * LIBRARY resolved names against and the started program still ran where its + * caller had been. Measured against a host, which answers the directory the + * caller entered. + * + * ⇒ This does not weaken the refusal openkal already makes. Declining to + * CHANGE a running program's working directory is right --- that is shared + * mutable state between execution contexts --- and that reason has never + * applied to stating, once, at the moment of starting, where a program runs. */ + struct kal_dir work; + + /* ⭐⭐ THE UNIT THIS PROGRAM BELONGS TO, AND IT IS THE ONE FIELD HERE THAT IS + * WRITTEN AS WELL AS READ. + * + * null this program belongs to no unit of the caller's making + * *job == 0 a new unit is formed, and its identity is written here + * *job != 0 this program joins the unit that word names + * + * ⚠️ IN AND OUT BECAUSE NEITHER KIND OF SYSTEM CAN DO IT THE OTHER WAY. This + * system creates the unit first and puts members into it; that one has the + * unit created BY its first member --- a process group's identity is a + * process's --- so there is nothing to create beforehand. An operation that + * opened an empty unit would be natural to one and impossible to the other, + * and clause 7.1 says which of the two is then at fault. Establishing the + * identity at the first start is the only shape both perform without keeping + * a table. + * + * ⇒ Unchanged if the start fails. An implementation that does not claim + * KAL_PROCESS_PROP_JOB reports kal_err_not_supported for a non-null `job' + * rather than starting a program outside the unit that was asked for. + * + * ⚠️ ENTERING A UNIT HAS A COST ON SOME SYSTEMS AND THAT IS WHY IT IS PER + * START. Where the unit is a process group, entering a new one LEAVES THE + * TERMINAL'S FOREGROUND GROUP, so a program with an interface whose child + * then reads the terminal stops on SIGTTIN. A caller that has given the + * started program pipes for all three streams wants a unit; an interactive + * caller must never be given one silently. + * + * ⚠️ AND THE TWO KINDS OF UNIT ARE NOT EQUALLY STRONG. A job is named by a + * handle that is never reused; a process group is named by a process + * identifier, which is reused once the leader has ended and the numbers have + * wrapped. Terminating a unit whose leader is long gone can therefore reach a + * different unit on such a system. This is what those systems do --- every + * program that calls `killpg' lives with it --- and it is recorded rather than + * hidden behind an interface that reads as though it were not so. */ + struct kal_job* job; + + /* The directories the started program receives, read back through + * `kal_fs_preopen'. A count of zero starts a program with no preopens at + * all, which is a different thing from not asking. */ + const struct kal_preopen* grants; + kal_uintptr grant_count; + + kal_uintptr flags; /* KAL_SPAWN_* */ +}; + +/* The started program does not outlive its caller: when the calling image ends, + * however it ends, the started program ends too. + * + * ⚠️ ADDED IN 0.10 BECAUSE A CALLER WAS TOLD A FALSEHOOD. Clause 7.1 declines to + * replace a running image, so a C library asked for `execve' composes it --- and + * the composition leaves THREE images where a system with the operation has two: + * the caller, a copy that waits, and the program. A signal reaches the middle + * one. Measured with a host as control: identical status words, opposite + * outcomes --- the caller is told the program died on the signal it sent while the + * program runs to completion, unsupervised. openkal-linux#13. */ +#define KAL_SPAWN_BOUND_LIFETIME ((kal_uintptr)1u << 0) + +/* A set of started programs that end together --- see `kal_spawn.job'. One + * machine word, as every handle here is. */ +struct kal_job { kal_uintptr h; }; #ifdef __cplusplus extern "C" { #endif -/* Starts the program named relative to a directory. The argument vector is - * complete and is passed unaltered: argv[0] is the name the started program - * observes as its own, and an implementation neither derives it from path nor - * prepends anything to the vector. Clause 7.6. */ -int kal_process_spawn(struct kal_dir base, +/* Starts a program. The argument vector is complete and is passed unaltered: + * argv[0] is the name the started program observes as its own, and an + * implementation neither derives it from path nor prepends anything to the + * vector. Clause 7.6. + * + * Everything that varies between one start and another is in `kal_spawn', which + * is why this is one declaration and not a family --- see the comment there. + * + * An implementation reports kal_err_not_supported, and starts nothing, when the + * request names something it cannot do: a flag whose position it does not claim + * in `kal_process_props', or a non-empty `grants' without + * KAL_PROCESS_PROP_GRANT_DIR. ⚠️ STARTING THE PROGRAM WITHOUT THE THING ASKED FOR + * IS NOT AN OPTION --- a caller that asked for a bound lifetime and got a program + * without one has been given a program that outlives it, which is the failure the + * flag exists to remove. */ +int kal_process_spawn(const struct kal_spawn* how, const char* path, kal_uintptr path_len, const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, const struct kal_spawn_streams* streams, struct kal_process* out); -/* Starts a program that receives exactly the directories named. - * - * The started program reads them back through `kal_fs_preopen', which is the - * operation this one is the inverse of --- clause 7.11. A count of zero starts - * a program with no preopens at all, which is a different thing from - * `kal_process_spawn' and is the whole reason a caller reaches for this. - * - * A SECOND DECLARATION RATHER THAN AN ARGUMENT ADDED TO THE FIRST, because - * clause 8 forbids altering an existing one. `kal_process_spawn' remains, and a - * program that does not grant directories keeps using it. */ -int kal_process_spawn_with(struct kal_dir base, - const char* path, kal_uintptr path_len, - const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, - const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, - const struct kal_spawn_streams* streams, - const struct kal_preopen* grants, kal_uintptr grant_count, - struct kal_process* out); - /* A pair of streams of which one end is intended to cross a spawn boundary. * * The caller holds `mine'; `theirs' is what it places in a @@ -125,54 +237,104 @@ int kal_process_channel(struct kal_stream* mine, struct kal_stream* theirs); void kal_process_channel_close(struct kal_stream s); int kal_process_wait(struct kal_process, int* status, int* terminated); + +/* Requests the termination of ONE started program, whatever unit it is in. + * + * ⭐ ITS MEANING DOES NOT DEPEND ON HOW THAT PROGRAM WAS STARTED, and 0.11 is + * where that became true. The shape this replaced was a flag on the spawn, after + * which this operation reached one program or a whole tree according to a + * property of the handle that no caller could see. An operation whose meaning + * turns on invisible state is the thing this interface exists to avoid, so the + * unit has an operation of its own and the caller says which it means. */ int kal_process_terminate(struct kal_process); void kal_process_close(struct kal_process); -/* Starts a program whose lifetime is BOUND to the caller's: when the calling - * image ends, however it ends, the started program ends too. Version 0.10. - * - * ⚠️⚠️ ADDED BECAUSE A LIBRARY ABOVE THIS INTERFACE HAD TO COMPOSE SOMETHING IT - * COULD NOT THEN CONTROL, AND A CALLER WAS TOLD A FALSEHOOD ABOUT IT. - * - * Clause 7.1 declines to replace a running image, correctly. A C library asked - * for `execve' therefore composes it: start the program, wait for it, end with - * its status. That composition leaves THREE images where a system with the - * operation has two --- the caller, the copy that is waiting, and the program. - * - * ⭐ AND THE MIDDLE ONE IS THE ONE A SIGNAL REACHES. `kal_process_terminate' - * upon the identifier the caller holds terminates the WAITER. Measured, with a - * host as control: identical status words, opposite outcomes --- the caller is - * told the program died on the signal it sent, while the program runs to - * completion, unsupervised. openkal-linux#13. - * - * ⚠️ `kal_process_terminate' is not at fault: it was asked to terminate one - * started program and did. What was missing was a way to SAY the thing `execve' - * means --- this program stands in for me, so it does not outlive me. - * - * Otherwise identical to kal_process_spawn. An implementation that cannot bind a - * lifetime does not claim KAL_PROCESS_PROP_BOUND_LIFETIME and reports - * kal_err_not_supported here rather than starting a program it cannot bind: a - * caller that asked for the binding asked for it, and a program started without - * it is not the program the caller asked to start. - * - * A SECOND DECLARATION RATHER THAN A FLAG ON THE FIRST, because clause 8 forbids - * altering an existing one --- the same reason `kal_process_spawn_with' is a - * declaration and not an argument. - * - * ⚠️ WHICH MEANS THE TWO ADDITIONS DO NOT COMBINE, AND THAT IS DELIBERATE. There - * is no form that both grants directories and binds a lifetime. Declaring every - * combination is how an interface acquires four spawns and then eight, so the - * combination is declared when something needs it and not before; clause 8 - * permits adding it then. What a caller must not do meanwhile is take - * `kal_process_spawn_with' and assume the binding: it does not bind, and a - * program that outlives its caller is exactly the failure this exists to - * remove. */ -int kal_process_spawn_bound(struct kal_dir base, - const char* path, kal_uintptr path_len, - const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, - const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, - const struct kal_spawn_streams* streams, - struct kal_process* out); +/* ⭐⭐ A WORD THIS PROGRAM'S ENVIRONMENT SETS WHEN SOMEBODY HAS ASKED IT TO END. + * Zero until then, non-zero afterwards, and never cleared. Null where an + * implementation does not claim KAL_PROCESS_PROP_STOP_REQUESTED. + * + * ⚠️ THIS IS NOT A SIGNAL INTERFACE, AND THE DIFFERENCE IS WHY IT CAN EXIST AT + * ALL. Signals are one environment's mechanism: a numbered set, a disposition per + * program, a handler that interrupts whatever was running. This system has them; + * that one has console control events, which arrive on a NEW EXECUTION CONTEXT + * and cover a different set of causes; two of the environments this + * specification targets have no processes to signal. An interface shaped like + * signals would be the borrowed shape clause 7.1 names. + * + * ⭐ WHAT WAS ACTUALLY MISSING WAS ONE SENTENCE, and it is the only one of the + * uses of signals that this interface could not already express: + * + * terminating a program kal_process_terminate + * terminating a unit kal_process_job_terminate + * the far end of a stream is gone reported by kal_stream_write + * a started program ended kal_process_wait + * a bound upon waiting openkal.timeout + * waking another context kal_task_wake + * THIS PROGRAM HAS BEEN ASKED TO END --- nothing, until now + * + * ⇒ A WORD AND NOT A HANDLER. A handler is a call into arbitrary code at an + * arbitrary point, which is the part of signals that does not travel: the other + * system runs its notification on a context of its own, and reproducing an + * interruption there would be a compatibility layer. A word is read when the + * program chooses, or WAITED UPON with `kal_task_wait' --- which this + * specification already has, and which `openkal.timeout' already bounds. No new + * concept, and none of the re-entrancy rules a handler forces on every caller. + * + * ⚠️ IT IS A NOTICE AND NOT A VETO. The program is told; whether it ends is its + * own affair, and it may still be ended afterwards by something it cannot + * observe --- which is true on every system this targets and is why the word says + * "requested" rather than "will happen". + * + * ⚠️ IT DOES NOT SAY WHO ASKED, OR HOW. One word, because a program that is + * asked to end does the same thing whoever asked. An implementation that can + * distinguish causes may add an enquiry later; this one does not need altering + * for that. + * + * ⚠️ AND IT SAYS NOTHING ABOUT AN END THAT CANNOT BE OBSERVED. Where a program is + * ended outright --- the un-declinable signal here, the abrupt termination there, + * `kal_process_job_terminate' through this interface --- nothing is set, because + * nothing anywhere gets to notice. That symmetry is the reason this is + * implementable on both kinds of system rather than on one. */ +const kal_u32* kal_process_stop_requested(void); + +/* Puts THE CALLING program into a unit, on the same terms as `kal_spawn.job': + * a word of zero forms a new unit and receives its identity, and a word that + * names one joins it. + * + * ⭐⭐ THE CALLER AND NOT A PROGRAM IT STARTS, WHICH IS THE WHOLE DIFFERENCE, and + * `kal_spawn.job' alone could not express it. + * + * A C library above this interface answers `fork'. The copy then wishes to be + * the start of a unit BEFORE it replaces itself --- which is what + * `setpgid(0, 0); exec…' means, and what every shell and every runner with a + * timeout is written as. Expressed only through the spawn, the unit formed + * belongs to the program the copy STARTS, whose identity the original never + * learns, so the original's request to end that unit names one that does not + * exist. + * + * ⚠️ THIS IS NOT THE MUTABLE AMBIENT STATE openkal DECLINES ELSEWHERE. A working + * directory that can be changed is shared between execution contexts and is + * refused for that reason. A unit is not shared and not read back: a program + * states once which unit it belongs to, and the only thing that can be done with + * the answer afterwards is to end the unit. + * + * An implementation that does not claim KAL_PROCESS_PROP_JOB reports + * kal_err_not_supported and leaves the word as it was. */ +int kal_process_job_enter(struct kal_job*); + +/* Requests the termination of every program in a unit, including programs + * started by its members that the caller never held a handle to. That last part + * is the whole reason a unit exists: a program that starts work in the + * background leaves nothing for a caller to terminate one at a time. */ +int kal_process_job_terminate(struct kal_job); + +/* Releases the caller's reference to a unit. ⚠️ IT DOES NOT END THE UNIT, and an + * implementation must take care that it does not: this system's job objects can + * be asked to end their members when the last handle closes, and one that asked + * for that would make this operation mean something different here from what it + * means where the unit is a process group and closing is releasing a number. + * Terminating is `kal_process_job_terminate' and nothing else is. */ +void kal_process_job_close(struct kal_job); kal_uintptr kal_process_props(void); diff --git a/include/openkal/space.h b/include/openkal/space.h index 87e2edb..fc14b85 100644 --- a/include/openkal/space.h +++ b/include/openkal/space.h @@ -26,8 +26,8 @@ * THERE IS NO OPERATION THAT CREATES AN EMPTY SPACE. An empty address space * contains no code, so the entry function a caller would name is not in it. A * program that wants a child holding only what it grants uses - * `kal_process_spawn_with', which starts a named program rather than a function - * of the caller's. */ + * `kal_process_spawn' with `grants' set, which starts a named program rather + * than a function of the caller's. */ #ifndef OPENKAL_SPACE_H #define OPENKAL_SPACE_H #include "types.h" diff --git a/include/openkal/version.h b/include/openkal/version.h index 7f5ddca..593de31 100644 --- a/include/openkal/version.h +++ b/include/openkal/version.h @@ -33,7 +33,7 @@ * older implementation reports conditions this consumer distinguishes as * conditions it does not, which is a wrong answer rather than a refusal. */ #define KAL_VERSION_MAJOR 0u -#define KAL_VERSION_MINOR 10u +#define KAL_VERSION_MINOR 11u #define KAL_VERSION_PATCH 0u #define KAL_VERSION_MAKE(major, minor, patch) \ diff --git a/mcpp.toml b/mcpp.toml index 1ea4902..bf0596f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.10.0" +version = "0.11.0" description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/src/macros.cppm b/src/macros.cppm new file mode 100644 index 0000000..731faff --- /dev/null +++ b/src/macros.cppm @@ -0,0 +1,118 @@ +// Every KAL_ macro, as a constant a module consumer can name. +// +// ⚠️⚠️ GENERATED BY tools/gen-macros.sh. Do not edit; CI regenerates this file +// and fails on a difference. +// +// A macro does not cross a module boundary. Twice now a capability word has been +// added to a header, used from a module consumer, and failed to compile there --- +// `KAL_LOCK_*' in 0.10 and `KAL_SPAWN_*' in 0.11. The interface modules give the +// well-named subset (`kal::fs::lock::exclusive'); this gives ALL of them under +// the C spelling with a `_M' suffix, so that a name in the header always exists +// here too and the two cannot drift. +module; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +export module openkal.macros; +export import openkal.types; + +export namespace kal::macros { +inline constexpr kal_uintptr KAL_DGRAM_PROP_BROADCAST_M = (kal_uintptr)(KAL_DGRAM_PROP_BROADCAST); +inline constexpr kal_uintptr KAL_DGRAM_PROP_IPV6_M = (kal_uintptr)(KAL_DGRAM_PROP_IPV6); +inline constexpr kal_uintptr KAL_EXEC_PROP_AVAILABLE_M = (kal_uintptr)(KAL_EXEC_PROP_AVAILABLE); +inline constexpr kal_uintptr KAL_EXEC_PROP_REPUBLISH_M = (kal_uintptr)(KAL_EXEC_PROP_REPUBLISH); +inline constexpr kal_uintptr KAL_FS_LINK_DIRECTORY_M = (kal_uintptr)(KAL_FS_LINK_DIRECTORY); +inline constexpr kal_uintptr KAL_FS_NO_RESOLVE_M = (kal_uintptr)(KAL_FS_NO_RESOLVE); +inline constexpr kal_uintptr KAL_FS_PROP_ATOMIC_RENAME_M = (kal_uintptr)(KAL_FS_PROP_ATOMIC_RENAME); +inline constexpr kal_uintptr KAL_FS_PROP_CAPACITY_M = (kal_uintptr)(KAL_FS_PROP_CAPACITY); +inline constexpr kal_uintptr KAL_FS_PROP_CASE_SENSITIVE_M = (kal_uintptr)(KAL_FS_PROP_CASE_SENSITIVE); +inline constexpr kal_uintptr KAL_FS_PROP_LINKS_M = (kal_uintptr)(KAL_FS_PROP_LINKS); +inline constexpr kal_uintptr KAL_FS_PROP_LOCKS_M = (kal_uintptr)(KAL_FS_PROP_LOCKS); +inline constexpr kal_uintptr KAL_FS_PROP_MAKE_LINKS_M = (kal_uintptr)(KAL_FS_PROP_MAKE_LINKS); +inline constexpr kal_uintptr KAL_FS_PROP_MODIFIED_TIME_M = (kal_uintptr)(KAL_FS_PROP_MODIFIED_TIME); +inline constexpr kal_uintptr KAL_IFACE_ABORT_M = (kal_uintptr)(KAL_IFACE_ABORT); +inline constexpr kal_uintptr KAL_IFACE_DATAGRAM_M = (kal_uintptr)(KAL_IFACE_DATAGRAM); +inline constexpr kal_uintptr KAL_IFACE_ENV_M = (kal_uintptr)(KAL_IFACE_ENV); +inline constexpr kal_uintptr KAL_IFACE_EXEC_M = (kal_uintptr)(KAL_IFACE_EXEC); +inline constexpr kal_uintptr KAL_IFACE_FS_M = (kal_uintptr)(KAL_IFACE_FS); +inline constexpr kal_uintptr KAL_IFACE_MEMORY_M = (kal_uintptr)(KAL_IFACE_MEMORY); +inline constexpr kal_uintptr KAL_IFACE_NET_M = (kal_uintptr)(KAL_IFACE_NET); +inline constexpr kal_uintptr KAL_IFACE_PROCESS_M = (kal_uintptr)(KAL_IFACE_PROCESS); +inline constexpr kal_uintptr KAL_IFACE_RANDOM_M = (kal_uintptr)(KAL_IFACE_RANDOM); +inline constexpr kal_uintptr KAL_IFACE_SPACE_M = (kal_uintptr)(KAL_IFACE_SPACE); +inline constexpr kal_uintptr KAL_IFACE_STREAM_M = (kal_uintptr)(KAL_IFACE_STREAM); +inline constexpr kal_uintptr KAL_IFACE_TASK_M = (kal_uintptr)(KAL_IFACE_TASK); +inline constexpr kal_uintptr KAL_IFACE_TERMINAL_M = (kal_uintptr)(KAL_IFACE_TERMINAL); +inline constexpr kal_uintptr KAL_IFACE_TIME_M = (kal_uintptr)(KAL_IFACE_TIME); +inline constexpr kal_uintptr KAL_IFACE_TIMEOUT_M = (kal_uintptr)(KAL_IFACE_TIMEOUT); +inline constexpr kal_uintptr KAL_INFO_ALL_M = (kal_uintptr)(KAL_INFO_ALL); +inline constexpr kal_uintptr KAL_INFO_IDENTITY_M = (kal_uintptr)(KAL_INFO_IDENTITY); +inline constexpr kal_uintptr KAL_INFO_KIND_M = (kal_uintptr)(KAL_INFO_KIND); +inline constexpr kal_uintptr KAL_INFO_MODIFIED_M = (kal_uintptr)(KAL_INFO_MODIFIED); +inline constexpr kal_uintptr KAL_INFO_SIZE_M = (kal_uintptr)(KAL_INFO_SIZE); +inline constexpr kal_uintptr KAL_INFO_WRITABLE_M = (kal_uintptr)(KAL_INFO_WRITABLE); +inline constexpr kal_uintptr KAL_LOCK_EXCLUSIVE_M = (kal_uintptr)(KAL_LOCK_EXCLUSIVE); +inline constexpr kal_uintptr KAL_LOCK_SHARED_M = (kal_uintptr)(KAL_LOCK_SHARED); +inline constexpr kal_uintptr KAL_LOCK_WAIT_M = (kal_uintptr)(KAL_LOCK_WAIT); +inline constexpr kal_uintptr KAL_NET_PROP_HALFCLOSE_M = (kal_uintptr)(KAL_NET_PROP_HALFCLOSE); +inline constexpr kal_uintptr KAL_NET_PROP_IPV6_M = (kal_uintptr)(KAL_NET_PROP_IPV6); +inline constexpr kal_uintptr KAL_OPEN_APPEND_M = (kal_uintptr)(KAL_OPEN_APPEND); +inline constexpr kal_uintptr KAL_OPEN_CREATE_M = (kal_uintptr)(KAL_OPEN_CREATE); +inline constexpr kal_uintptr KAL_OPEN_EXCLUSIVE_M = (kal_uintptr)(KAL_OPEN_EXCLUSIVE); +inline constexpr kal_uintptr KAL_OPEN_READ_M = (kal_uintptr)(KAL_OPEN_READ); +inline constexpr kal_uintptr KAL_OPEN_TRUNCATE_M = (kal_uintptr)(KAL_OPEN_TRUNCATE); +inline constexpr kal_uintptr KAL_OPEN_WRITE_M = (kal_uintptr)(KAL_OPEN_WRITE); +inline constexpr kal_uintptr KAL_PROCESS_PROP_BOUND_LIFETIME_M = (kal_uintptr)(KAL_PROCESS_PROP_BOUND_LIFETIME); +inline constexpr kal_uintptr KAL_PROCESS_PROP_CHANNEL_M = (kal_uintptr)(KAL_PROCESS_PROP_CHANNEL); +inline constexpr kal_uintptr KAL_PROCESS_PROP_EXIT_STATUS_M = (kal_uintptr)(KAL_PROCESS_PROP_EXIT_STATUS); +inline constexpr kal_uintptr KAL_PROCESS_PROP_GRANT_DIR_M = (kal_uintptr)(KAL_PROCESS_PROP_GRANT_DIR); +inline constexpr kal_uintptr KAL_PROCESS_PROP_JOB_M = (kal_uintptr)(KAL_PROCESS_PROP_JOB); +inline constexpr kal_uintptr KAL_PROCESS_PROP_STOP_REQUESTED_M = (kal_uintptr)(KAL_PROCESS_PROP_STOP_REQUESTED); +inline constexpr kal_uintptr KAL_PROCESS_PROP_STREAM_PASSING_M = (kal_uintptr)(KAL_PROCESS_PROP_STREAM_PASSING); +inline constexpr kal_uintptr KAL_PROCESS_PROP_TERMINATE_M = (kal_uintptr)(KAL_PROCESS_PROP_TERMINATE); +inline constexpr kal_uintptr KAL_RANDOM_PROP_BLOCKING_M = (kal_uintptr)(KAL_RANDOM_PROP_BLOCKING); +inline constexpr kal_uintptr KAL_RANDOM_PROP_HARDWARE_M = (kal_uintptr)(KAL_RANDOM_PROP_HARDWARE); +inline constexpr kal_uintptr KAL_SEEK_CURRENT_M = (kal_uintptr)(KAL_SEEK_CURRENT); +inline constexpr kal_uintptr KAL_SEEK_END_M = (kal_uintptr)(KAL_SEEK_END); +inline constexpr kal_uintptr KAL_SEEK_SET_M = (kal_uintptr)(KAL_SEEK_SET); +inline constexpr kal_uintptr KAL_SHUT_BOTH_M = (kal_uintptr)(KAL_SHUT_BOTH); +inline constexpr kal_uintptr KAL_SHUT_READ_M = (kal_uintptr)(KAL_SHUT_READ); +inline constexpr kal_uintptr KAL_SHUT_WRITE_M = (kal_uintptr)(KAL_SHUT_WRITE); +inline constexpr kal_uintptr KAL_SPACE_PROP_CLONE_HANDLES_M = (kal_uintptr)(KAL_SPACE_PROP_CLONE_HANDLES); +inline constexpr kal_uintptr KAL_SPACE_PROP_DEFERRED_COPY_M = (kal_uintptr)(KAL_SPACE_PROP_DEFERRED_COPY); +inline constexpr kal_uintptr KAL_SPAWN_BOUND_LIFETIME_M = (kal_uintptr)(KAL_SPAWN_BOUND_LIFETIME); +inline constexpr kal_uintptr KAL_STREAM_PROP_INTERACTIVE_M = (kal_uintptr)(KAL_STREAM_PROP_INTERACTIVE); +inline constexpr kal_uintptr KAL_TASK_PROP_PARALLEL_M = (kal_uintptr)(KAL_TASK_PROP_PARALLEL); +inline constexpr kal_uintptr KAL_TASK_PROP_PREEMPTIVE_M = (kal_uintptr)(KAL_TASK_PROP_PREEMPTIVE); +inline constexpr kal_uintptr KAL_TASK_PROP_THREAD_LOCAL_M = (kal_uintptr)(KAL_TASK_PROP_THREAD_LOCAL); +inline constexpr kal_uintptr KAL_TASK_PROP_WAIT_TIMEOUT_M = (kal_uintptr)(KAL_TASK_PROP_WAIT_TIMEOUT); +inline constexpr kal_uintptr KAL_TERM_ECHO_M = (kal_uintptr)(KAL_TERM_ECHO); +inline constexpr kal_uintptr KAL_TERM_LINE_EDIT_M = (kal_uintptr)(KAL_TERM_LINE_EDIT); +inline constexpr kal_uintptr KAL_TERM_PROP_MODE_M = (kal_uintptr)(KAL_TERM_PROP_MODE); +inline constexpr kal_uintptr KAL_TERM_PROP_SIZE_M = (kal_uintptr)(KAL_TERM_PROP_SIZE); +inline constexpr kal_uintptr KAL_TIME_PROP_MONOTONIC_SUSPENDS_M = (kal_uintptr)(KAL_TIME_PROP_MONOTONIC_SUSPENDS); +inline constexpr kal_uintptr KAL_TIME_PROP_SLEEP_PRECISE_M = (kal_uintptr)(KAL_TIME_PROP_SLEEP_PRECISE); +inline constexpr kal_uintptr KAL_TIME_PROP_WALL_AVAILABLE_M = (kal_uintptr)(KAL_TIME_PROP_WALL_AVAILABLE); +inline constexpr kal_uintptr KAL_VERSION_M = (kal_uintptr)(KAL_VERSION); +inline constexpr kal_uintptr KAL_VERSION_MAJOR_M = (kal_uintptr)(KAL_VERSION_MAJOR); +inline constexpr kal_uintptr KAL_VERSION_MINOR_M = (kal_uintptr)(KAL_VERSION_MINOR); +inline constexpr kal_uintptr KAL_VERSION_PATCH_M = (kal_uintptr)(KAL_VERSION_PATCH); +} // namespace kal::macros + +// Function-like, and therefore not constants --- listed so that their absence +// above is a stated fact rather than a gap: +// KAL_VERSION_MAKE diff --git a/src/process.cppm b/src/process.cppm index 2ed3666..c1ffa57 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -22,16 +22,20 @@ export import openkal.stream; export using ::kal_process; export using ::kal_spawn_streams; +export using ::kal_spawn; +export using ::kal_job; export using ::kal_preopen; export using ::kal_process_spawn; -export using ::kal_process_spawn_with; -export using ::kal_process_spawn_bound; export using ::kal_process_channel; export using ::kal_process_channel_close; export using ::kal_process_wait; export using ::kal_process_terminate; export using ::kal_process_close; +export using ::kal_process_job_enter; +export using ::kal_process_job_terminate; +export using ::kal_process_job_close; +export using ::kal_process_stop_requested; export using ::kal_process_props; static_assert(sizeof(kal_process) == sizeof(kal_uintptr), "clause 7.2"); @@ -52,6 +56,23 @@ inline constexpr props exit_status {KAL_PROCESS_PROP_EXIT_STATUS}; inline constexpr props channel {KAL_PROCESS_PROP_CHANNEL}; inline constexpr props grant_dir {KAL_PROCESS_PROP_GRANT_DIR}; inline constexpr props bound_lifetime{KAL_PROCESS_PROP_BOUND_LIFETIME}; +inline constexpr props job {KAL_PROCESS_PROP_JOB}; +inline constexpr props stop_requested{KAL_PROCESS_PROP_STOP_REQUESTED}; + +// ⚠️ WHAT A CALLER ASKS FOR, WHICH IS A DIFFERENT WORD FROM WHAT AN +// IMPLEMENTATION CAN DO. `props' above answers the second; these set the first. +// A module consumer sees neither unless both are named here --- the C spellings +// are macros, and a macro is invisible across a module boundary. +struct spawn_flag_tag; +using spawn_flags = kal::props; + +inline constexpr spawn_flags bound_lifetime_flag{KAL_SPAWN_BOUND_LIFETIME}; + +using how = kal_spawn; +using job_handle = kal_job; + +static_assert(sizeof(kal_job) == sizeof(kal_uintptr), "clause 7.2"); +static_assert(sizeof(kal_spawn) == 6 * sizeof(kal_uintptr), "clause 5.3"); inline props properties() { return props{kal_process_props()}; } inline bool has(props p) { return properties().has(p); } diff --git a/tools/gen-macros.sh b/tools/gen-macros.sh new file mode 100755 index 0000000..3c77397 --- /dev/null +++ b/tools/gen-macros.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Generate src/macros.cppm from the headers. +# +# ⚠️⚠️ THIS EXISTS BECAUSE THE SAME DEFECT HAPPENED TWICE. `KAL_LOCK_*' in 0.10 +# and `KAL_SPAWN_*' in 0.11 were both added to a header, both used from a module +# consumer, and both failed to compile there --- because a macro DOES NOT CROSS A +# MODULE BOUNDARY, and nothing checked that every macro had a module spelling. +# +# ⇒ So the mapping is generated rather than written. A macro added to a header +# and not to this module is not a thing anyone has to remember: CI regenerates +# and diffs, so forgetting is a red build rather than a consumer's compile error +# two repositories away. +# +# Object-like macros only. A function-like one (`KAL_VERSION_MAKE') is not a +# constant and has no constexpr spelling; those are listed as skipped so that +# "absent" is never silently the same as "not applicable". +set -euo pipefail + +cd "$(dirname "$0")/.." +out=src/macros.cppm + +names=$(grep -rhoE '^#define +KAL_[A-Z0-9_]+' include/openkal/*.h \ + | awk '{print $2}' | sort -u) +skipped=$(grep -rhoE '^#define +KAL_[A-Z0-9_]+\(' include/openkal/*.h \ + | awk '{print $2}' | sed 's/(.*//' | sort -u) + +{ +cat <<'HEAD' +// Every KAL_ macro, as a constant a module consumer can name. +// +// ⚠️⚠️ GENERATED BY tools/gen-macros.sh. Do not edit; CI regenerates this file +// and fails on a difference. +// +// A macro does not cross a module boundary. Twice now a capability word has been +// added to a header, used from a module consumer, and failed to compile there --- +// `KAL_LOCK_*' in 0.10 and `KAL_SPAWN_*' in 0.11. The interface modules give the +// well-named subset (`kal::fs::lock::exclusive'); this gives ALL of them under +// the C spelling with a `_M' suffix, so that a name in the header always exists +// here too and the two cannot drift. +module; +HEAD + +# Every header, so that a macro cannot be missed by an include list that was not +# updated either --- the same failure one level down. +for h in $(ls include/openkal/*.h | xargs -n1 basename | sort); do + printf '#include \n' "$h" +done + +cat <<'MID' +export module openkal.macros; +export import openkal.types; + +export namespace kal::macros { +MID + +# ⚠️ THE DECLARED NAME CANNOT BE THE MACRO'S NAME: the macro is still defined +# here, so `constexpr kal_uintptr KAL_VERSION_MAJOR = …' expands on the LEFT as +# well and becomes `constexpr kal_uintptr 0u = …'. +# +# ⭐ `_M' RATHER THAN LOWER-CASING. The C spelling is what a reader is looking +# for --- they arrived here because the header said `KAL_SPAWN_OWN_JOB' --- so the +# name is kept and marked. `KAL_SPAWN_OWN_JOB_M' is a different token, so the +# preprocessor leaves it alone, and the suffix says what it is: the module +# spelling of a macro. +for n in $names; do + case " $skipped " in *" $n "*) continue;; esac + printf 'inline constexpr kal_uintptr %-34s = (kal_uintptr)(%s);\n' "${n}_M" "$n" +done + +echo '} // namespace kal::macros' +echo +echo '// Function-like, and therefore not constants --- listed so that their absence' +echo '// above is a stated fact rather than a gap:' +for n in $skipped; do echo "// $n"; done +} > "$out" + +echo "wrote $out: $(grep -c '^inline constexpr' "$out") constants, $(echo "$skipped" | wc -w) function-like skipped"