From 8b3df104d869d74b46c77da6f2bfda0a79ba4f8e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 02:05:18 +0900 Subject: [PATCH 1/2] 0.22.0 --- the null device at a position of a started program (KAL_SPAWN_NULL_IN, _OUT, _ERR) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .../docs/2026-11-03-openkal-0.22-design.md | 77 +++++++++++++++++++ README.md | 8 +- SPEC.md | 40 +++++++++- SURFACE.txt | 2 +- conformance/src/macros_reach.cpp | 4 + conformance/src/sections/child.cpp | 35 +++++++++ conformance/src/sections/child.cppm | 8 ++ conformance/src/sections/process.cpp | 45 ++++++++++- include/openkal/process.h | 20 +++++ include/openkal/version.h | 2 +- mcpp.toml | 2 +- src/macros.cppm | 4 + src/process.cppm | 4 + 13 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 .agents/docs/2026-11-03-openkal-0.22-design.md diff --git a/.agents/docs/2026-11-03-openkal-0.22-design.md b/.agents/docs/2026-11-03-openkal-0.22-design.md new file mode 100644 index 0000000..d0ce045 --- /dev/null +++ b/.agents/docs/2026-11-03-openkal-0.22-design.md @@ -0,0 +1,77 @@ +# openkal 0.22:位置上的空设备 + +2026-11-03。状态:**实施所依据的方案**。规范对照基于 `SPEC.md` 0.21,结果为 0.22.0。 +来源:mcpp 生态设计 2026-11-02 part 3(ok 映像的性能对等)§16.7、§17.5;mcppls-clangd #16 的 warm-prime 与 mcppls #75 的 +Windows 端到端测试。 + +--- + +## 1. 要解决的问题 + +clangd 作为 ok 映像在 Windows 上为每个模块启动 worker,全部失败:`failed to launch module worker: posix_spawn failed: No such +file or directory`。 + +| # | 现象 | 原因 | +| --- | --- | --- | +| N1 | 映像中 `open("/dev/null")` 为 `ENOENT`,Linux 上同一映像成功 | openkal-musl 按 preopen 解析名字;Windows 的 preopen 是卷(`C:/`…),没有名为 `/` 的目录 | +| N2 | `posix_spawn` 在加入 `addopen(…, "/dev/null", …)` 后为 `ENOENT` | LLVM 的 `ExecuteNoWait` 把空路径的重定向写成 `/dev/null`;worker 的标准输入与输出都是空重定向 | + +空设备是每个启动程序的环境都有的资源,各自以自己的写法命名:Linux、macOS 为 `/dev/null`,Windows 为 `NUL`。openkal 不命名 +它所提供的目录之外的任何东西,程序无法经由本接口按名字到达它。 + +## 2. 两层的分工 + +| 用法 | 所需 | 归属 | +| --- | --- | --- | +| 程序自己读写空设备 | 读得到输入结束、写全部接受:纯语义,无需环境的对象 | C 库(openkal-musl 0.27) | +| 把空设备交给启动的程序 | 启动的程序在该位置持有一个环境的句柄;它可能不是 openkal 程序 | openkal(本版本) | + +C 库可以用已有的接口拼出第二种用法:输入给一个远端已释放的通道,输出给一个由调用者的任务持续读空的通道。这一拼法与空设备 +不等价:调用者先结束时,启动的程序的写入以错误结束(Linux 上默认被 SIGPIPE 结束);每个字节经过管道并由调用者复制;每个程序 +的每个位置占用调用者的一个任务;启动的程序看到的是管道而不是字符设备。mcpplibs.os 0.2.7 的 `stdio::null` 正是这样拼出的,并 +以 `host() != windows` 选择路径,即在 openkal 之上的库里按系统名改变调用方式。 + +## 3. 增补 + +| 接口 | 增补 | 层级 | 条文 | +| --- | --- | --- | --- | +| `openkal.process` | `KAL_SPAWN_NULL_IN`、`KAL_SPAWN_NULL_OUT`、`KAL_SPAWN_NULL_ERR`;属性 `KAL_PROCESS_PROP_NULL_STREAMS` | 属性位 | 7.15、11 条 47 | + +位置上的标志使该位置得到环境的空设备:启动的程序在该位置读到输入结束,写入全部接受并丢弃。未声明属性位的实现报告 +`kal_err_not_supported` 且不启动程序;同一位置上标志与非零的流同时出现为 `kal_err_invalid`。 + +## 4. 方案中的决定 + +1. **启动的标志,而不是一个流。** 返回一个空设备流的操作需要与之配套的释放(`openkal.stream` 没有释放,通道的释放是 + `kal_process_channel_close`),且程序自己读写空设备时 C 库无需环境即可回答;需要环境的只有交给启动的程序这一种用法,它属于 + 启动。`kal_spawn` 的布局不变(`flags` 已有),`SURFACE.txt` 不增加名字,加载器(okrun)的绑定表不变。 +2. **三个位置各一个标志。** 三个位置互相独立,C 库的文件动作逐个位置给出;一个"全部为空"的标志无法表达"输入为空、输出为管道"。 +3. **属性位,而不是必需。** 不启动程序的环境(openkal-emscripten、openkal-opensbi)没有可交付的对象;声明 `openkal.process` + 的三个实现都声明该位。 +4. **设备在启动的程序一侧打开。** Linux、macOS 的实现在替换前的映像(复制体)中打开 `/dev/null` 并放到位置上,调用者不持有其 + 描述符;Windows 的实现为本次启动打开 `NUL`,按调用者的流的方式标记为可继承并列入 `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`, + 启动后释放。7.13(启动的程序只得到三个流与授予的目录)不变。 +5. **名字留在 C 库。** openkal-musl 把 POSIX 的 `/dev/null` 解析为进程内的空描述符,并在 `posix_spawn` 的文件动作中把它映射为 + 相应位置的标志;`NUL` 这一写法只出现在 openkal-windows 中。 + +## 5. 版本组 + +| 包 | 版本 | 内容 | +| --- | --- | --- | +| openkal | 0.22.0 | 本文 | +| openkal-linux | 0.23.0 | 实现;`tests/openkal_0_22.cpp` | +| openkal-macos | 0.20.0 | 实现;`tests/openkal_0_22.cpp` | +| openkal-windows | 0.18.0 | 实现;`tests/null_device_at_a_position.cpp` | +| openkal-emscripten | 0.11.0 | 依 0.22 编写,无其他改动 | +| openkal-image | 0.3.2 | 在上述实现上重建 okrun 与 okld | +| openkal-musl | 0.27.0 | `/dev/null` 与 `posix_spawn` 的映射 | +| openkal-llvm-runtime | 0.18.0 | 建在 openkal-musl 0.27.0 上 | +| openkal-platform | 0.22.0 | 版本组 | +| mcpplibs.os | 0.2.8 | `stdio::null` 改用这些标志 | + +## 6. 验证 + +- 一致性套件:声明该位的实现,启动的副本在三个位置得到空设备时读到输入结束、写入全部接受(状态 60);标志与非零的流同处一个 + 位置为 `kal_err_invalid`;未声明该位的实现拒绝该标志;能力字只含规范分配的位置。openkal-linux 0.23 的本地一致性:324 项全部成立。 +- 实现自己的测试:启动的程序在该位置看到字符设备(Linux、macOS 为 `fstat` 的 `S_IFCHR`,Windows 为 `GetFileType` 的 + `FILE_TYPE_CHAR`),读到输入结束,写入被接受;启动前后调用者的描述符或句柄数相同。Windows 的测试在 Wine 下通过。 diff --git a/README.md b/README.md index 743a111..15a0bba 100644 --- a/README.md +++ b/README.md @@ -77,16 +77,16 @@ conditional on the target. ```toml [dependencies] -openkal = "0.21.0" +openkal = "0.22.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.22.0" +openkal-linux = "0.23.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.19.0" +openkal-macos = "0.20.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.17.0" +openkal-windows = "0.18.0" ``` The program imports the interface and names no implementation. diff --git a/SPEC.md b/SPEC.md index cd105ba..c0378e8 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.21 +# openkal Specification, version 0.22 ## 1. Scope @@ -92,7 +92,7 @@ provides an interface in whole or not at all. Version 0.15 specifies the core and optional interfaces, version 0.18 adds `openkal.map` (clause 11, entry 40), and version 0.20 adds `openkal.region`, -`openkal.prof` and `openkal.debug` (entries 44 and 45); version 0.21 adds no interface. The reserved row is not specified, and its +`openkal.prof` and `openkal.debug` (entries 44 and 45); versions 0.21 and 0.22 add no interface. 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 @@ -1030,6 +1030,26 @@ A volume that cannot remove a held name at once is answered before the call (clause 6.2): the position is not claimed, and a caller that needs the directory gone closes what it holds first. +### 7.15 The null device at a position + +A position of `kal_spawn_streams` is inherited, or receives a stream the caller +placed. `KAL_SPAWN_NULL_IN`, `KAL_SPAWN_NULL_OUT` and `KAL_SPAWN_NULL_ERR` give +it the environment's null device: at that position the started program reads +the end of input, and every write is accepted in full and discarded. An +implementation claims the three with `KAL_PROCESS_PROP_NULL_STREAMS`; one that +does not reports `kal_err_not_supported` and starts nothing. A flag beside a +non-zero stream at the same position is `kal_err_invalid`. + +The device is a resource of every environment that starts programs, and each +names it in its own spelling: `/dev/null` on Linux and macOS, `NUL` on Windows. +This specification names nothing outside the directories it supplies, so the +device is reached by the position it is given at and not by a name; a C library +maps its own spelling of the device onto the flags. A library cannot compose the +device from what precedes it: a placed stream is a channel or a file, and a +channel's far end either ends the started program's writes with an error once +its reader is released, or keeps a reader in the caller for as long as the +program runs. + ## 8. Evolution Each interface is versioned independently. A revision may add declarations and @@ -1882,6 +1902,22 @@ The following are recorded so that they are not mistaken for oversights. its handle is held (`kal_program_info`), as its peak memory does since 0.20: every environment keeps it in the record of the ended program, and a C library's `wait4` reports the two together. +47. **The null device at a position.** **Specified in 0.22**, clause 7.15, as + `KAL_SPAWN_NULL_IN`, `KAL_SPAWN_NULL_OUT`, `KAL_SPAWN_NULL_ERR` and + `KAL_PROCESS_PROP_NULL_STREAMS` in `openkal.process` (`process.h`). A + language server run as an image started each worker that builds a module + with its standard input and output sent to `/dev/null`, as LLVM's + `ExecuteNoWait` sends an empty redirection; on the one environment whose + preopens are volumes and not a root, the C library found no such name, and + every start failed with `ENOENT`. The device is the environment's and not + the library's: a started program needs a handle of the environment at the + position, which a library could otherwise supply only as a channel drained + by the caller for as long as the program runs. It is a flag of the start + and not a stream a program opens, because a stream would need a release of + its own beside `kal_process_channel_close`, and a program that reads or + writes the device itself is served by its C library without the + environment. It is a position and not required, because an environment + that starts no programs has nothing to give it to. ## 12. Images diff --git a/SURFACE.txt b/SURFACE.txt index 67c35a5..748800f 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.21.0, one name per line. +# The C surface of openkal 0.22.0, 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 diff --git a/conformance/src/macros_reach.cpp b/conformance/src/macros_reach.cpp index 73a27c8..427310f 100644 --- a/conformance/src/macros_reach.cpp +++ b/conformance/src/macros_reach.cpp @@ -41,5 +41,9 @@ static_assert(kal::macros::KAL_PROCESS_PROP_STOP_REQUESTED_M == 128u); static_assert(kal::macros::KAL_SPAWN_BOUND_LIFETIME_M == 1u); static_assert(kal::macros::KAL_SPAWN_ONE_STRING_M == 2u); static_assert(kal::macros::KAL_PROCESS_PROP_ONE_STRING_M == 512u); +static_assert(kal::macros::KAL_SPAWN_NULL_IN_M == 4u); +static_assert(kal::macros::KAL_SPAWN_NULL_OUT_M == 8u); +static_assert(kal::macros::KAL_SPAWN_NULL_ERR_M == 16u); +static_assert(kal::macros::KAL_PROCESS_PROP_NULL_STREAMS_M == 8192u); } // namespace diff --git a/conformance/src/sections/child.cpp b/conformance/src/sections/child.cpp index 2b0c6e2..560b45c 100644 --- a/conformance/src/sections/child.cpp +++ b/conformance/src/sections/child.cpp @@ -41,6 +41,7 @@ const char* argument_for(errand e) { case errand::map_beyond_shortened: return "--child-map-beyond-shortened"; case errand::hold_memory: return "--child-hold-memory"; case errand::grow_memory: return "--child-grow-memory"; + case errand::read_and_write_null: return "--child-read-and-write-null"; case errand::none: break; } return ""; @@ -64,6 +65,7 @@ errand child_errand() { if (same(a, len, argument_for(errand::map_beyond_shortened))) return errand::map_beyond_shortened; if (same(a, len, argument_for(errand::hold_memory))) return errand::hold_memory; if (same(a, len, argument_for(errand::grow_memory))) return errand::grow_memory; + if (same(a, len, argument_for(errand::read_and_write_null))) return errand::read_and_write_null; } #endif return errand::none; @@ -224,6 +226,20 @@ after g_after; } #endif kal_exit(45); + case errand::read_and_write_null: + // The three positions hold the null device. 61: a read did not + // report the end of input. 62, 63: a write to standard output or + // standard error was not accepted in full. 60: all three held. +#ifdef MCPP_FEATURE_CORE + { + char buf[16]; + if (kal_stream_read(kal_stdin(), buf, sizeof buf) != 0) kal_exit(61); + if (kal_stream_write(kal_stdout(), "discarded\n", 10) != 10) kal_exit(62); + if (kal_stream_write(kal_stderr(), "discarded\n", 10) != 10) kal_exit(63); + kal_exit(60); + } +#endif + kal_exit(64); case errand::hold_memory: case errand::grow_memory: // Memory written through, so that every environment charges it --- @@ -387,12 +403,31 @@ bool start_copy_granting(const char* errand_argument, const kal_preopen* grants, return e == kal_ok; } +bool start_copy_with(const char* errand_argument, kal_uintptr flags, + const kal_spawn_streams& streams, int& spawned, + int& status, int& terminated) { + kal_dir base{}; const char* rel = nullptr; kal_uintptr rel_len = 0; + if (!locate_self(base, rel, rel_len)) return false; + const char* argv[2] = { "openkal-conformance-child", errand_argument }; + kal_uintptr lens[2]; + for (int i = 0; i < 2; ++i) { kal_uintptr n = 0; while (argv[i][n]) ++n; lens[i] = n; } + const kal_spawn how{ base, kal::fs::working(), nullptr, nullptr, 0, flags }; + kal_process child{}; + spawned = kal_process_spawn(&how, rel, rel_len, argv, lens, 2, nullptr, nullptr, 0, + &streams, &child); + if (spawned != kal_ok) return true; + const int e = kal_process_wait(child, &status, &terminated); + kal_process_close(child); + return e == kal_ok; +} + #else bool start_copy(const char*, const char*, int&, int&) { return false; } bool start_copy_running(const char*, const char*, kal_process&) { return false; } bool start_copy_in_unit(const char*, kal_job&, kal_process&) { return false; } bool start_copy_granting(const char*, const kal_preopen*, kal_uintptr, int&, int&) { return false; } +bool start_copy_with(const char*, kal_uintptr, const kal_spawn_streams&, int&, int&, int&) { return false; } #endif diff --git a/conformance/src/sections/child.cppm b/conformance/src/sections/child.cppm index 790f4a6..8af7379 100644 --- a/conformance/src/sections/child.cppm +++ b/conformance/src/sections/child.cppm @@ -30,6 +30,8 @@ enum class errand { hold_memory, // holds 32 MiB written through, then waits to be ended (0.20) grow_memory, // grows by 8 MiB at a time to 512 MiB, then waits; ends // itself with 77 after twenty seconds (0.20) + read_and_write_null, // reads the end of input and writes in full at its three + // positions, given the null device there (0.22) }; // The names the starter grants and the copy expects, in order, and the marker @@ -79,4 +81,10 @@ bool start_copy_in_unit(const char* errand_argument, kal_job& unit, kal_process& bool start_copy_granting(const char* errand_argument, const kal_preopen* grants, kal_uintptr count, int& status, int& terminated); +// The same, with the given flags and streams; `spawned' is what the start +// answered, and the copy is awaited only when it started (0.22). +bool start_copy_with(const char* errand_argument, kal_uintptr flags, + const kal_spawn_streams& streams, int& spawned, + int& status, int& terminated); + } // namespace okc diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 4c4ed46..1be3564 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -398,7 +398,8 @@ void run() { | kal::process::one_string | kal::process::job_usage | kal::process::job_limit_hard - | kal::process::job_limit_supervised).bits; + | kal::process::job_limit_supervised + | kal::process::null_streams).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), @@ -475,6 +476,48 @@ void run() { "the implementation claims prop_one_string"); } + // THE NULL DEVICE AT A POSITION (0.22, clause 7.15). A copy given it at + // all three positions reads the end of input and has every write + // accepted; a flag beside a stream at its position is invalid; an + // implementation that does not claim the flags refuses them. + { + const kal_uintptr all = (kal::process::null_in_flag | kal::process::null_out_flag + | kal::process::null_err_flag).bits; + if (kal::process::has(kal::process::null_streams)) { + const kal_spawn_streams inherit{ {0}, {0}, {0} }; + int spawned = -1, status = -1, terminated = -1; + const bool ran = start_copy_with(argument_for(errand::read_and_write_null), all, + inherit, spawned, status, terminated); + observe(kind::behaviour, ran && spawned == kal_ok && terminated == 0 && status == 60, + "a position given the null device reads the end of input and accepts every write"); + kal_stream mine{}, theirs{}; + if (kal::process::has(kal::process::channel) + && kal_process_channel(&mine, &theirs) == kal_ok) { + const kal_spawn_streams placed{ {0}, theirs, {0} }; + spawned = -1; + start_copy_with(argument_for(errand::exit_with_33), + kal::process::null_out_flag.bits, placed, spawned, status, terminated); + kal_process_channel_close(theirs); + kal_process_channel_close(mine); + observe(kind::behaviour, spawned == kal_err_invalid, + "the null device beside a stream at one position is invalid"); + } else { + unobserved(kind::behaviour, "the null device beside a stream at one position is invalid", + "the implementation provides no channel"); + } + } else { + kal_process p{}; + const char* argv[1] = { "x" }; + const kal_uintptr lens[1] = { 1 }; + const kal_spawn how{ kal::fs::working(), kal::fs::working(), nullptr, + nullptr, 0, all }; + 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, + "the null device this implementation cannot give is refused, not ignored"); + } + } + // 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. diff --git a/include/openkal/process.h b/include/openkal/process.h index 06c744b..84c5ceb 100644 --- a/include/openkal/process.h +++ b/include/openkal/process.h @@ -94,6 +94,9 @@ struct kal_preopen { * limit by watching what it uses: the unit may exceed it by what it allocates * between two observations before it is ended. Version 0.20. */ #define KAL_PROCESS_PROP_JOB_LIMIT_SUPERVISED ((kal_uintptr)1u << 12) +/* KAL_SPAWN_NULL_IN, KAL_SPAWN_NULL_OUT and KAL_SPAWN_NULL_ERR are answered + * here. Version 0.22. */ +#define KAL_PROCESS_PROP_NULL_STREAMS ((kal_uintptr)1u << 13) /* HOW A PROGRAM IS STARTED, AND IT IS ONE OPERATION BECAUSE 0.11 STOPPED * MAKING IT A FAMILY. @@ -244,6 +247,23 @@ struct kal_spawn { * vector answers kal_err_not_supported: the request means nothing there. */ #define KAL_SPAWN_ONE_STRING ((kal_uintptr)1u << 1) +/* THE ENVIRONMENT'S NULL DEVICE AT A POSITION. Version 0.22, clause 7.15. + * + * A position of `kal_spawn_streams' is inherited (zero) or receives a stream the + * caller placed. These give it a third answer: at that position the started + * program reads the end of input, and what it writes there is accepted and + * discarded. Every environment that starts programs has such a device and names + * it in its own spelling --- `/dev/null' on Linux and macOS, `NUL' on Windows --- + * and a program cannot reach it through this interface by a name, because the + * interface names nothing outside the directories it supplies. A C library maps + * its own spelling of the device onto these. + * + * A flag beside a non-zero stream at the same position is kal_err_invalid: the + * two answers exclude each other. */ +#define KAL_SPAWN_NULL_IN ((kal_uintptr)1u << 2) +#define KAL_SPAWN_NULL_OUT ((kal_uintptr)1u << 3) +#define KAL_SPAWN_NULL_ERR ((kal_uintptr)1u << 4) + /* 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; }; diff --git a/include/openkal/version.h b/include/openkal/version.h index 68d76d5..f68e50d 100644 --- a/include/openkal/version.h +++ b/include/openkal/version.h @@ -42,7 +42,7 @@ * for the reason tools/check-readme-versions.sh exists --- the one fact a * consumer actually asks for was checked by nobody. */ #define KAL_VERSION_MAJOR 0u -#define KAL_VERSION_MINOR 21u +#define KAL_VERSION_MINOR 22u #define KAL_VERSION_PATCH 0u #define KAL_VERSION_MAKE(major, minor, patch) \ diff --git a/mcpp.toml b/mcpp.toml index 7a0d3b7..5906cca 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.21.0" +version = "0.22.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 index 79a76bc..69c53fd 100644 --- a/src/macros.cppm +++ b/src/macros.cppm @@ -109,6 +109,7 @@ inline constexpr kal_uintptr KAL_PROCESS_PROP_JOB_M = (kal_uintptr)( inline constexpr kal_uintptr KAL_PROCESS_PROP_JOB_LIMIT_HARD_M = (kal_uintptr)(KAL_PROCESS_PROP_JOB_LIMIT_HARD); inline constexpr kal_uintptr KAL_PROCESS_PROP_JOB_LIMIT_SUPERVISED_M = (kal_uintptr)(KAL_PROCESS_PROP_JOB_LIMIT_SUPERVISED); inline constexpr kal_uintptr KAL_PROCESS_PROP_JOB_USAGE_M = (kal_uintptr)(KAL_PROCESS_PROP_JOB_USAGE); +inline constexpr kal_uintptr KAL_PROCESS_PROP_NULL_STREAMS_M = (kal_uintptr)(KAL_PROCESS_PROP_NULL_STREAMS); inline constexpr kal_uintptr KAL_PROCESS_PROP_ONE_STRING_M = (kal_uintptr)(KAL_PROCESS_PROP_ONE_STRING); inline constexpr kal_uintptr KAL_PROCESS_PROP_OPEN_ID_M = (kal_uintptr)(KAL_PROCESS_PROP_OPEN_ID); inline constexpr kal_uintptr KAL_PROCESS_PROP_STOP_REQUESTED_M = (kal_uintptr)(KAL_PROCESS_PROP_STOP_REQUESTED); @@ -128,6 +129,9 @@ inline constexpr kal_uintptr KAL_SHUT_WRITE_M = (kal_uintptr)( 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_SPAWN_NULL_ERR_M = (kal_uintptr)(KAL_SPAWN_NULL_ERR); +inline constexpr kal_uintptr KAL_SPAWN_NULL_IN_M = (kal_uintptr)(KAL_SPAWN_NULL_IN); +inline constexpr kal_uintptr KAL_SPAWN_NULL_OUT_M = (kal_uintptr)(KAL_SPAWN_NULL_OUT); inline constexpr kal_uintptr KAL_SPAWN_ONE_STRING_M = (kal_uintptr)(KAL_SPAWN_ONE_STRING); inline constexpr kal_uintptr KAL_STREAM_PROP_INTERACTIVE_M = (kal_uintptr)(KAL_STREAM_PROP_INTERACTIVE); inline constexpr kal_uintptr KAL_TASK_PRIORITY_BACKGROUND_M = (kal_uintptr)(KAL_TASK_PRIORITY_BACKGROUND); diff --git a/src/process.cppm b/src/process.cppm index 6967b1d..5c8a503 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -82,6 +82,7 @@ inline constexpr props one_string {KAL_PROCESS_PROP_ONE_STRING}; // 0.17.1 inline constexpr props job_usage {KAL_PROCESS_PROP_JOB_USAGE}; // 0.20 inline constexpr props job_limit_hard{KAL_PROCESS_PROP_JOB_LIMIT_HARD}; // 0.20 inline constexpr props job_limit_supervised{KAL_PROCESS_PROP_JOB_LIMIT_SUPERVISED}; // 0.20 +inline constexpr props null_streams {KAL_PROCESS_PROP_NULL_STREAMS}; // 0.22 // The fields of kal_program_info, for `wanted' and `present' (0.17). namespace info { @@ -102,6 +103,9 @@ using spawn_flags = kal::props; inline constexpr spawn_flags bound_lifetime_flag{KAL_SPAWN_BOUND_LIFETIME}; inline constexpr spawn_flags one_string_flag{KAL_SPAWN_ONE_STRING}; // 0.17.1 +inline constexpr spawn_flags null_in_flag {KAL_SPAWN_NULL_IN}; // 0.22 +inline constexpr spawn_flags null_out_flag{KAL_SPAWN_NULL_OUT}; // 0.22 +inline constexpr spawn_flags null_err_flag{KAL_SPAWN_NULL_ERR}; // 0.22 using how = kal_spawn; using job_handle = kal_job; From 9723538b9f2a1e52ae564c7777b5e08c34602b74 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 02:20:02 +0900 Subject: [PATCH 2/2] SPEC 7.15: the device occupies its position and leaves the caller holding nothing of it (clause 7.13); a file keeps what is written Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- SPEC.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/SPEC.md b/SPEC.md index c0378e8..943e253 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1038,17 +1038,19 @@ it the environment's null device: at that position the started program reads the end of input, and every write is accepted in full and discarded. An implementation claims the three with `KAL_PROCESS_PROP_NULL_STREAMS`; one that does not reports `kal_err_not_supported` and starts nothing. A flag beside a -non-zero stream at the same position is `kal_err_invalid`. +non-zero stream at the same position is `kal_err_invalid`. The device occupies +its position and is not a further handle (clause 7.13), and an implementation +that opens it for the start leaves the caller holding nothing of it. The device is a resource of every environment that starts programs, and each names it in its own spelling: `/dev/null` on Linux and macOS, `NUL` on Windows. This specification names nothing outside the directories it supplies, so the device is reached by the position it is given at and not by a name; a C library maps its own spelling of the device onto the flags. A library cannot compose the -device from what precedes it: a placed stream is a channel or a file, and a -channel's far end either ends the started program's writes with an error once -its reader is released, or keeps a reader in the caller for as long as the -program runs. +device from what precedes it: a placed stream is a file's or a channel's end; a +file keeps what is written, and a channel's far end either ends the started +program's writes with an error once its reader is released, or keeps a reader in +the caller for as long as the program runs. ## 8. Evolution