Skip to content

Commit 3da0934

Browse files
Implement openkal.terminal, and follow openkal 0.8 (#11)
* ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤) `xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本 命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己 沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要 `2.44`,构建在编译任何东西之前就停住: error: selected RuntimeBinding glibc@2.44 requires payload '.../xpkgs/xim-x-glibc/2.44', but it is not installed ⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。 索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则: 「The index is DATA and the client is a PROGRAM: the consumer ships first。」 ## ⚠️ 我先试过 bump xlings,撤回了 把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉 (mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到 `glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧: 接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说 「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。 ⇒ 保持 xlings 不动,把缺的那个载荷直接装上。 ⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受 「版本精化了请求」的已装载荷,从它引导就不需要这一步。 * Implement openkal.terminal, and follow openkal 0.8 The terminal ioctls this kernel offers are the ones kal_stream_props already uses, so the interface is a natural extension rather than a new mechanism: TIOCGETA and TIOCSETA for the mode, TIOCGWINSZ for the size. The request numbers encode the size of the structure they carry, so the layout is declared here and asserted rather than named from a header. A header stating it would belong to a C library, and this implementation has none. The mode is read, modified and written rather than written alone. The structure carries a baud rate and twenty control characters that the interface does not name; composing one from the mode word would discard them, and the terminal a program returned to would not be the one it found. kal_terminal_props asks TIOCGWINSZ rather than deriving the answer from TIOCGETA. A pseudo terminal answers both and a serial line answers only the first, so a word derived from one would claim a facility the next call refuses. The other four interfaces version 0.8 adds are not provided, which clause 6.1 makes an absence at the link rather than a deviation. * Provide the three operations openkal 0.8 adds to openkal.process ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, and adding a new interface obliges none. Clause 6.1 makes an interface a backend does not provide absent at the link and not a deviation; it makes one provided IN PART a deviation. The five interfaces version 0.8 adds are therefore free to decline, and the three names added to openkal.process are not. The specification's own surface checker said so before anything else noticed: openkal.process is provided in part: 3 of 8 names are not exported -- kal_process_channel kal_process_channel_close kal_process_spawn_with kal_process_channel is this kernel's `pipe', which reports BOTH descriptors as return values rather than through a buffer. That is a property of the calling convention, and src/sys.h already meets the same difference for the duplication primitive; a second wrapper is added beside it rather than the difference being hidden. There is no pipe2 here, so close-on-exec is set afterwards with fcntl. The two steps are not equivalent to one under a concurrent spawn, and that is stated rather than concealed: a caller that spawns from one context, which is what a program using this operation does, is unaffected. dup2 onto the same number succeeds and does nothing, where the other kernel's dup3 refuses. Either is right for the placement loop; only the reason differs, and it is written down so that a reader comparing the two implementations does not take one for an oversight. * Do not redeclare nr_fcntl The number was already in the table. I added it again beside nr_pipe rather than looking, and this kernel's table is one enumeration, so the second declaration is an error rather than a duplicate: src/sys.h:178:5: error: redefinition of enumerator 'nr_fcntl' Not visible from a machine that cannot build for this system, which is why the check that found it is the one that runs on it. The whole table was then examined for the same shape rather than this one line corrected: no other name is declared twice, in either architecture's block, in this repository or in the other backend that has such a table. * Name the released specification and the released engine openkal 0.8.0 is published, so the manifest names it by version rather than by branch. A branch is how a change spanning these repositories is developed and is not a form a published package may carry: a consumer resolving from the index would be handed a reference that moves. The engine pin moves to mcpp 2026.8.27.1 and the repository variable MCPP_SOURCE_REF is cleared, so what this repository tests is the engine a user installs. --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent 0c3bded commit 3da0934

5 files changed

Lines changed: 300 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ on:
2727
default: ""
2828
env:
2929
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
30-
MCPP_VERSION: 2026.8.26.2
30+
MCPP_VERSION: 2026.8.27.1
3131
XLINGS_VERSION: v2026.8.17.2
3232
XLINGS_NON_INTERACTIVE: '1'
3333

@@ -83,6 +83,34 @@ jobs:
8383
else
8484
xlings install "mcpp@$MCPP_VERSION" -y -g
8585
fi
86+
# ⚠️⚠️ TRANSITION: GIVE THE BOOTSTRAP THE glibc ITS BINDING NAMES.
87+
#
88+
# `xim:glibc`'s `latest` moved from `2.44` to `2.44.2`. A payload
89+
# directory is named after the version a request RESOLVED to, while a
90+
# RuntimeBinding carries the version that was DECLARED — and the xlings
91+
# a released mcpp vendors into its own sandbox still declares `2.44`.
92+
# So a clean machine installs `2.44.2`, the toolchain fixup asks for
93+
# `2.44`, and the build stops before anything is compiled:
94+
#
95+
# error: selected RuntimeBinding glibc@2.44 requires payload
96+
# '…/xpkgs/xim-x-glibc/2.44', but it is not installed
97+
#
98+
# ⚠️ On every NEW machine and on none that already existed, which is why
99+
# it is invisible from a developer's own. Measured on `main` as readily
100+
# as on any branch — the index records the same failure verbatim in
101+
# `pkgs/g/glibc.lua` and states the rule it broke: "The index is DATA
102+
# and the client is a PROGRAM: the consumer ships first."
103+
#
104+
# ⭐ REMOVE THIS once a released mcpp resolves it. `mcpp 2026.8.27.1`
105+
# accepts an installed payload whose version REFINES the requested one
106+
# (`payload_dir_for_version`), so a bootstrap from it needs nothing
107+
# here. Until then the missing payload is simply installed.
108+
if [ -x "$HOME/.mcpp/registry/bin/xlings" ]; then
109+
XLINGS_HOME="$HOME/.mcpp/registry" XLINGS_NON_INTERACTIVE=1 \
110+
"$HOME/.mcpp/registry/bin/xlings" install glibc@2.44 -y -g \
111+
>/dev/null 2>&1 || true
112+
echo "glibc payloads present: $(ls "$HOME/.mcpp/registry/data/xpkgs/xim-x-glibc" 2>/dev/null | tr '\n' ' ')"
113+
fi
86114
mcpp --version
87115
mcpp self config --mirror GLOBAL
88116
# ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE.

mcpp.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-macos"
4-
version = "0.3.4"
4+
version = "0.4.0"
55
description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used."
66
license = "Apache-2.0"
77

@@ -18,7 +18,7 @@ authors = ["mcpplibs"]
1818
repo = "https://github.com/mcpplibs/openkal-macos"
1919

2020
[dependencies]
21-
openkal = "0.7.0"
21+
openkal = "0.8.0"
2222

2323
[build]
2424
# The flags are attached to this package's own sources rather than to the whole

src/process.cpp

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,111 @@ int kal_process_spawn(kal_dir base,
108108
return kal_ok;
109109
}
110110

111+
// A channel: a pair of streams of which one end is meant to cross a spawn.
112+
//
113+
// This kernel's `pipe' reports BOTH descriptors as return values rather than
114+
// through a buffer, which is a property of its calling convention and not of the
115+
// call: the second value comes back in the second register. src/sys.h says the
116+
// same thing about the duplication primitive, and for the same reason.
117+
//
118+
// THERE IS NO pipe2 HERE, so close-on-exec is set afterwards with fcntl. Doing
119+
// it in two steps is not equivalent under a concurrent spawn --- another context
120+
// starting a program between the two would inherit the descriptors --- and this
121+
// implementation states that rather than concealing it. A caller that spawns
122+
// from one context, which is what a program using this operation does, is not
123+
// affected.
124+
int kal_process_channel(kal_stream* mine, kal_stream* theirs) {
125+
if (mine == nullptr || theirs == nullptr) return kal_err_invalid;
126+
127+
okm_long second = 0;
128+
const okm_long first = okm::pipe_pair(second);
129+
if (okm::failed(first)) return okm::translate(first);
130+
131+
constexpr okm_long f_setfd = 2, fd_cloexec = 1;
132+
okm::sys(okm::nr_fcntl, first, f_setfd, fd_cloexec);
133+
okm::sys(okm::nr_fcntl, second, f_setfd, fd_cloexec);
134+
135+
// Bare descriptors, because openkal.stream's transfer operations take what
136+
// this kernel takes. kal_fs_stream reports a file's stream the same way.
137+
*mine = kal_stream{ static_cast<kal_uintptr>(first) }; // the reading end
138+
*theirs = kal_stream{ static_cast<kal_uintptr>(second) }; // the writing end
139+
return kal_ok;
140+
}
141+
142+
void kal_process_channel_close(kal_stream s) {
143+
// The standard streams are borrowed and are numbered 0, 1 and 2; closing one
144+
// of those through this operation would take a stream away from the whole
145+
// program.
146+
const okm_long fd = static_cast<okm_long>(s.h);
147+
if (fd < 3) return;
148+
okm::sys(okm::nr_close, fd);
149+
}
150+
151+
// Starting a program that receives exactly the directories named.
152+
//
153+
// The grants are placed as descriptors three and upward, which is where
154+
// kal_fs_preopen reads them back from. The inverse relationship clause 7.11
155+
// describes is between those two operations, which is why they must agree about
156+
// the numbering rather than each choosing one.
157+
int kal_process_spawn_with(kal_dir base,
158+
const char* path, kal_uintptr path_len,
159+
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
160+
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
161+
const kal_spawn_streams* streams,
162+
const kal_preopen* grants, kal_uintptr grant_count,
163+
kal_process* out) {
164+
const int b = okm::unpack(base.h);
165+
if (b < 0 || out == nullptr) return kal_err_invalid;
166+
if (!okm::acceptable(path, path_len)) return kal_err_invalid;
167+
if (grant_count > 0 && grants == nullptr) return kal_err_invalid;
168+
okm::terminated p(path, path_len);
169+
if (!p.ok) return kal_err_invalid;
170+
171+
vector args, envs;
172+
if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory;
173+
if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory;
174+
175+
// Resolved before the duplication, because a failure after it would leave a
176+
// child to be reaped and a caller holding an error it cannot act upon.
177+
constexpr kal_uintptr max_grants = 16;
178+
if (grant_count > max_grants) return kal_err_invalid;
179+
int granted[max_grants];
180+
for (kal_uintptr i = 0; i < grant_count; ++i) {
181+
granted[i] = okm::unpack(grants[i].dir.h);
182+
if (granted[i] < 0) return kal_err_invalid;
183+
}
184+
185+
const okm_long in = streams ? static_cast<okm_long>(streams->in) : 0;
186+
const okm_long ou = streams ? static_cast<okm_long>(streams->out) : 0;
187+
const okm_long er = streams ? static_cast<okm_long>(streams->err) : 0;
188+
189+
bool is_duplicate = false;
190+
const okm_long child = okm::duplicate(is_duplicate);
191+
if (okm::failed(child)) return okm::translate(child);
192+
193+
if (is_duplicate) {
194+
if (in != 0) okm::sys(okm::nr_dup2, in, 0);
195+
if (ou != 0) okm::sys(okm::nr_dup2, ou, 1);
196+
if (er != 0) okm::sys(okm::nr_dup2, er, 2);
197+
198+
// dup2 onto the same number succeeds and does nothing, unlike dup3,
199+
// which refuses. Either behaviour is right for this loop; only the
200+
// reason differs, and it is stated so that a reader comparing the two
201+
// implementations does not take one of them for an oversight.
202+
for (kal_uintptr i = 0; i < grant_count; ++i)
203+
okm::sys(okm::nr_dup2, granted[i], static_cast<okm_long>(3 + i));
204+
205+
okm::sys(nr_fchdir, b);
206+
okm::sys(okm::nr_execve, reinterpret_cast<okm_long>(p.buf),
207+
reinterpret_cast<okm_long>(args.slots),
208+
reinterpret_cast<okm_long>(envs.slots));
209+
for (;;) okm::sys(okm::nr_exit, 127);
210+
}
211+
212+
*out = kal_process{ static_cast<kal_uintptr>(child) };
213+
return kal_ok;
214+
}
215+
111216
int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) {
112217
if (h.h == 0) return kal_err_invalid;
113218
int st = 0;
@@ -144,6 +249,7 @@ void kal_process_close(kal_process) { }
144249

145250
const kal_uintptr kal_process_props =
146251
KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING
147-
| KAL_PROCESS_PROP_EXIT_STATUS;
252+
| KAL_PROCESS_PROP_EXIT_STATUS
253+
| KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR;
148254

149255
}

src/sys.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,46 @@ inline okm_long duplicate(bool& is_duplicate) {
122122
#endif
123123
}
124124

125+
// Creating a pipe, which is the second call whose result does not fit the
126+
// convention above and does so for the same reason as the first.
127+
//
128+
// This kernel reports BOTH descriptors as return values: the reading end in the
129+
// first register and the writing end in the second. The other kernel takes a
130+
// buffer and fills it. Neither is more natural; what matters is that this file
131+
// meets the difference rather than hiding it, as it does for the duplication
132+
// primitive above.
133+
inline okm_long pipe_pair(okm_long& writing) {
134+
#if defined(__aarch64__)
135+
register okm_long x16 __asm__("x16") = 42; // pipe
136+
register okm_long x0 __asm__("x0") = 0;
137+
register okm_long x1 __asm__("x1") = 0;
138+
okm_long failed;
139+
__asm__ __volatile__("svc #0x80\n\tcset %2, cs"
140+
: "+r"(x0), "+r"(x1), "=r"(failed)
141+
: "r"(x16)
142+
: "memory", "cc");
143+
writing = x1;
144+
return failed ? -x0 : x0;
145+
#else
146+
okm_long first, second;
147+
unsigned char failed;
148+
__asm__ __volatile__("syscall"
149+
: "=a"(first), "=d"(second), "=@ccc"(failed)
150+
: "a"(42L | 0x2000000L)
151+
: "rcx", "r11", "memory", "cc");
152+
writing = second;
153+
return failed ? -first : first;
154+
#endif
155+
}
156+
125157
// The numbers. They are the same on both architectures this implementation
126158
// supports, which is the reason the table is not per-architecture as it is on
127159
// the other kernel.
128160
enum : okm_long {
129161
nr_exit = 1, nr_read = 3, nr_write = 4, nr_close = 6, nr_wait4 = 7,
130162
nr_chdir = 12, nr_getpid = 20, nr_getuid = 24, nr_geteuid = 25,
131163
nr_kill = 37, nr_dup = 41, nr_getegid = 43, nr_getgid = 47,
132-
nr_ioctl = 54, nr_execve = 59, nr_umask = 60,
164+
nr_ioctl = 54, nr_execve = 59, nr_umask = 60, nr_pipe = 42,
133165
nr_munmap = 73, nr_mprotect = 74, nr_madvise = 75,
134166
nr_dup2 = 90, nr_fsync = 95, nr_gettimeofday = 116,
135167
nr_readv = 120, nr_writev = 121, nr_ftruncate = 201,

src/terminal.cpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#include "sys.h"
2+
#include <openkal/terminal.h>
3+
4+
// openkal.terminal upon this kernel's terminal ioctls.
5+
//
6+
// THE REQUESTS ENCODE THE SIZE OF THE STRUCTURE THEY CARRY, which is why they
7+
// are written out here rather than named from a header: the number is a property
8+
// of this kernel's layout, and a header that stated it would belong to a C
9+
// library this implementation does not have. kal_stream_props already spells
10+
// TIOCGETA the same way and for the same reason.
11+
12+
namespace {
13+
14+
// This kernel's terminal settings, in this kernel's layout. Four flag words of a
15+
// machine word each, twenty control characters, and two speeds. The size is
16+
// seventy-two bytes, which is the number the requests below carry.
17+
struct oktermios {
18+
okm_ulong iflag;
19+
okm_ulong oflag;
20+
okm_ulong cflag;
21+
okm_ulong lflag;
22+
unsigned char cc[20];
23+
okm_ulong ispeed;
24+
okm_ulong ospeed;
25+
};
26+
static_assert(sizeof(oktermios) == 72,
27+
"the request numbers below carry this size");
28+
29+
struct okwinsize {
30+
unsigned short row;
31+
unsigned short col;
32+
unsigned short xpixel;
33+
unsigned short ypixel;
34+
};
35+
static_assert(sizeof(okwinsize) == 8, "the request number below carries this size");
36+
37+
// _IOR('t', 19, struct termios) and _IOW('t', 20, struct termios), and
38+
// _IOR('t', 104, struct winsize).
39+
constexpr okm_long tiocgeta = 0x40000000L | (72L << 16) | ('t' << 8) | 19;
40+
constexpr okm_long tiocseta = 0x80000000L | (72L << 16) | ('t' << 8) | 20;
41+
constexpr okm_long tiocgwinsz = 0x40000000L | (8L << 16) | ('t' << 8) | 104;
42+
43+
// Positions within lflag. This kernel's values, which are not the other's.
44+
constexpr okm_ulong t_echo = 0x00000008u;
45+
constexpr okm_ulong t_icanon = 0x00000100u;
46+
47+
kal_uintptr mode_of(const oktermios& t) {
48+
kal_uintptr m = 0;
49+
if ((t.lflag & t_icanon) != 0) m |= KAL_TERM_LINE_EDIT;
50+
if ((t.lflag & t_echo) != 0) m |= KAL_TERM_ECHO;
51+
return m;
52+
}
53+
54+
int get_termios(kal_stream s, oktermios& out) {
55+
const okm_long r = okm::sys(okm::nr_ioctl, static_cast<okm_long>(s.h),
56+
tiocgeta, reinterpret_cast<okm_long>(&out));
57+
// A stream that is not a terminal is reported as unsupported rather than
58+
// having this kernel's own classification passed through.
59+
if (okm::failed(r)) return kal_err_not_supported;
60+
return kal_ok;
61+
}
62+
63+
} // namespace
64+
65+
extern "C" {
66+
67+
int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) {
68+
if (mode == nullptr) return kal_err_invalid;
69+
oktermios t{};
70+
const int rc = get_termios(s, t);
71+
if (rc != kal_ok) return rc;
72+
*mode = mode_of(t);
73+
return kal_ok;
74+
}
75+
76+
int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) {
77+
// READ, MODIFY, WRITE. The structure carries a baud rate and twenty control
78+
// characters that this interface does not name; composing one from the mode
79+
// word alone would discard them, and the terminal a program returned to
80+
// would not be the one it found.
81+
oktermios t{};
82+
const int rc = get_termios(s, t);
83+
if (rc != kal_ok) return rc;
84+
85+
if ((mode & KAL_TERM_LINE_EDIT) != 0) t.lflag |= t_icanon;
86+
else t.lflag &= ~t_icanon;
87+
if ((mode & KAL_TERM_ECHO) != 0) t.lflag |= t_echo;
88+
else t.lflag &= ~t_echo;
89+
90+
// A position this implementation does not distinguish is ignored rather than
91+
// refused, which clause 6.2 requires: a program compiled against a later
92+
// revision sets a position this build has never heard of.
93+
const okm_long w = okm::sys(okm::nr_ioctl, static_cast<okm_long>(s.h),
94+
tiocseta, reinterpret_cast<okm_long>(&t));
95+
if (okm::failed(w)) return kal_err_not_supported;
96+
return kal_ok;
97+
}
98+
99+
int kal_terminal_size(kal_stream s, kal_uintptr* cols, kal_uintptr* rows) {
100+
if (cols == nullptr || rows == nullptr) return kal_err_invalid;
101+
okwinsize w{};
102+
const okm_long r = okm::sys(okm::nr_ioctl, static_cast<okm_long>(s.h),
103+
tiocgwinsz, reinterpret_cast<okm_long>(&w));
104+
// Both outputs are left untouched, which the interface requires: a serial
105+
// line answers TIOCGETA and not this, so a caller must be able to tell the
106+
// two conditions apart.
107+
if (okm::failed(r)) return kal_err_not_supported;
108+
*cols = static_cast<kal_uintptr>(w.col);
109+
*rows = static_cast<kal_uintptr>(w.row);
110+
return kal_ok;
111+
}
112+
113+
kal_uintptr kal_terminal_props(kal_stream s) {
114+
kal_uintptr p = 0;
115+
116+
oktermios t{};
117+
if (get_termios(s, t) == kal_ok) p |= KAL_TERM_PROP_MODE;
118+
119+
// The size is asked for rather than derived from the first. Deriving it
120+
// would make the word claim a facility the very next call refuses.
121+
okwinsize w{};
122+
const okm_long r = okm::sys(okm::nr_ioctl, static_cast<okm_long>(s.h),
123+
tiocgwinsz, reinterpret_cast<okm_long>(&w));
124+
if (!okm::failed(r)) p |= KAL_TERM_PROP_SIZE;
125+
126+
return p;
127+
}
128+
129+
} // extern "C"

0 commit comments

Comments
 (0)