From bd5ad2045d9b4ee9129c1ef42ce599e47078aeb7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 16:20:20 +0800 Subject: [PATCH 1/2] 0.7.0 --- openkal 0.10's five operations, four implemented and one refused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⭐⭐ THE ONE THAT IS REFUSED IS THE POINT OF THIS COMMIT. `kal_process_spawn_bound' asks that a started program not outlive its caller, however the caller ends --- including when it is killed outright. This system has no primitive that arms that from inside the started image; the other kernel does, in one call. ⚠️ WHAT THIS SYSTEM HAS INSTEAD IS A WATCH, AND A WATCH IS NOT THAT. A context here can be told when another ends and can then act --- but a watch needs a live context to notice, so a caller that is killed outright notices nothing and the started program survives. That is precisely the failure the operation exists to remove, so composing it would move the defect from "refused" to "works except when it matters". `KAL_PROCESS_PROP_BOUND_LIFETIME' is not claimed and the operation reports kal_err_not_supported, which a caller can ask about first. --- the four that are implemented ------------------------------------------ ⚠️ AND TWO OF THEM WOULD HAVE COMPILED, RUN, AND DONE THE WRONG THING IF COPIED FROM THE SIBLING IMPLEMENTATION. `struct flock' here puts the POSITIONS first and the kinds last; the other kernel's puts the kinds first. A structure copied across would place a 64-bit offset where two shorts belong. And the kinds themselves are the BSD numbering: a read lock is 1 here and 0 there, a write lock is 3 here and 1 there. A table copied across would take the wrong KIND of lock and report success. `kal_fs_lock'/`kal_fs_unlock' use the open-file form (`F_OFD_*', 10.10 onward), because openkal states the holder as the `kal_file' and this system's oldest form holds it by the process --- releasing every lock when any descriptor for the node closes. `kal_fs_set_modified_at' opens the name for READING and uses this system's descriptor-taking call. Opening for reading is enough for it and is what lets a DIRECTORY be reached, which is the whole reason the declaration exists. `kal_fs_capacity' reports bytes from `f_bavail' rather than `f_bfree' --- what this program could use, not what the volume has. `kal_task_parallelism' asks `hw.ncpu'; zero is "cannot say" and is not one. --- README.md | 2 +- mcpp.toml | 4 +-- src/fs.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++++++++- src/process.cpp | 25 +++++++++++++ src/sys.h | 26 ++++++++++++++ src/task.cpp | 18 ++++++++++ 6 files changed, 164 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d7eb4d7..d839f03 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ written on the kernel's own calls. openkal = "0.9.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.6.1" +openkal-macos = "0.7.0" ``` Its purpose is as much to test the specification as to be used. A specification diff --git a/mcpp.toml b/mcpp.toml index 4d5bdae..64bb9af 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.6.1" +version = "0.7.0" 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." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-macos" [dependencies] -openkal = "0.9.0" +openkal = "0.10.0" [build] # The flags are attached to this package's own sources rather than to the whole diff --git a/src/fs.cpp b/src/fs.cpp index b5ea7c6..d005cef 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -275,6 +275,93 @@ int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { return okm::failed(r) ? okm::translate(r) : kal_ok; } +// The modification time of a NAME, including a directory. Version 0.10. +// +// ⚠️ Expressed as opening the name and using the operation above's own call, +// because this system's time-setting call takes a descriptor. Opening for +// READING is enough for it and is what lets a DIRECTORY be reached --- which is +// the whole reason this declaration exists: the file-taking form takes a +// `kal_file' and a directory is a `kal_dir', so this interface had no route to +// a directory's time at all. +int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, + kal_u64 modified_ns) { + const int b = okm::unpack(base.h); + if (b < 0 || !okm::acceptable(name, len)) return kal_err_invalid; + okm::terminated t(name, len); if (!t.ok) return kal_err_invalid; + + const okm_long fd = okm::sys(okm::nr_openat, b, + reinterpret_cast(t.buf), + okm::o_rdonly | okm::o_cloexec, 0); + if (okm::failed(fd)) return okm::translate(fd); + + okm::kstat64 st{}; + okm_long r = okm::sys(okm::nr_fstat64, fd, reinterpret_cast(&st)); + if (!okm::failed(r)) { + okm::ktimeval times[2]; + times[0].sec = st.atime_sec; + times[0].usec = static_cast(st.atime_nsec / 1000); + times[0].pad = 0; + times[1].sec = static_cast(modified_ns / 1000000000u); + times[1].usec = static_cast((modified_ns % 1000000000u) / 1000u); + times[1].pad = 0; + r = okm::sys(okm::nr_futimes, fd, reinterpret_cast(times)); + } + okm::sys(okm::nr_close, fd); + return okm::failed(r) ? okm::translate(r) : kal_ok; +} + +// --- exclusion upon a range of a file --------------------------------------- +// +// ⭐ THE OPEN-FILE FORM. This system's oldest record lock is held by the process +// and is released when that process closes any descriptor for the node; openkal +// states the holder as the `kal_file', and `F_OFD_*' is exactly that. +static int lock_range(kal_file f, kal_u64 start, kal_u64 len, + short type, bool wait) { + const int fd = okm::unpack(f.h); + if (fd < 0) return kal_err_invalid; + + okm::kflock fl{}; + fl.l_start = static_cast(start); + fl.l_len = static_cast(len); // zero means to the end, as here + fl.l_pid = 0; + fl.l_type = type; + fl.l_whence = okm::seek_set; + + const okm_long cmd = wait ? okm::f_ofd_setlkw : okm::f_ofd_setlk; + okm_long r; + do { + r = okm::sys(okm::nr_fcntl, fd, cmd, reinterpret_cast(&fl)); + } while (r == -okm::e_intr); + return okm::failed(r) ? okm::translate(r) : kal_ok; +} + +int kal_fs_lock(kal_file f, kal_u64 start, kal_u64 len, kal_uintptr mode) { + const bool shared = (mode & KAL_LOCK_SHARED) != 0; + const bool exclusive = (mode & KAL_LOCK_EXCLUSIVE) != 0; + if (shared == exclusive) return kal_err_invalid; + return lock_range(f, start, len, + shared ? okm::lock_read : okm::lock_write, + (mode & KAL_LOCK_WAIT) != 0); +} + +int kal_fs_unlock(kal_file f, kal_u64 start, kal_u64 len) { + return lock_range(f, start, len, okm::lock_unlock, false); +} + +// How much the volume holds, in bytes. `f_bavail' rather than `f_bfree': the +// question is what THIS program could use. +int kal_fs_capacity(kal_dir d, kal_u64* total, kal_u64* available) { + const int fd = okm::unpack(d.h); + if (fd < 0) return kal_err_invalid; + okm::kstatfs64 sf{}; + const okm_long r = okm::sys(okm::nr_fstatfs64, fd, reinterpret_cast(&sf)); + if (okm::failed(r)) return okm::translate(r); + const kal_u64 unit = static_cast(sf.f_bsize); + if (total) *total = static_cast(sf.f_blocks) * unit; + if (available) *available = static_cast(sf.f_bavail) * unit; + return kal_ok; +} + int kal_fs_mkdir(kal_dir base, const char* name, kal_uintptr len) { const int b = okm::unpack(base.h); if (b < 0 || !okm::acceptable(name, len)) return kal_err_invalid; @@ -380,8 +467,12 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, // mounts, and `renameat' within one directory is atomic by POSIX. kal_uintptr kal_fs_props(kal_dir d) { const int fd = okm::unpack(d.h); + // Locks and capacity are answered by this system's VFS for every format + // beneath it, so they are in the conservative set rather than switched on + // by name below. const kal_uintptr conservative = - KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; + KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME + | KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY; if (fd < 0) return 0; okm::kstatfs64 sf{}; diff --git a/src/process.cpp b/src/process.cpp index fac60c2..54defc1 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -247,6 +247,31 @@ int kal_process_terminate(kal_process h) { // waited for continues, and this environment collects it when the caller exits. void kal_process_close(kal_process) { } +// Starting a program whose lifetime is bound to this one's. Version 0.10. +// +// ⚠️⚠️ REFUSED HERE, AND THE REFUSAL IS THE HONEST ANSWER RATHER THAN A GAP TO +// FILL LATER WITH SOMETHING THAT LOOKS LIKE IT. +// +// The binding openkal describes has to hold however the caller ends, including +// when it is killed outright --- and this system has no primitive that arms it +// from inside the started image. The other kernel does, in one call. +// +// ⚠️ What this system offers instead is a WATCH: a context here can be told when +// another ends and can then act. That is not the same thing and must not be +// offered as it. A watch needs a live context to notice, so a caller that is +// killed outright notices nothing and the started program survives --- which is +// precisely the failure the operation exists to remove. Composing it would move +// the defect from "refused" to "works except when it matters". +// +// ⇒ `KAL_PROCESS_PROP_BOUND_LIFETIME' is not claimed, and a caller that asks +// first is told before it depends on it. +int kal_process_spawn_bound(kal_dir, const char*, kal_uintptr, + const char**, const kal_uintptr*, kal_uintptr, + const char**, const kal_uintptr*, kal_uintptr, + const kal_spawn_streams*, kal_process*) { + return kal_err_not_supported; +} + kal_uintptr kal_process_props(void) { return KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING | KAL_PROCESS_PROP_EXIT_STATUS diff --git a/src/sys.h b/src/sys.h index 9a70117..d5e50aa 100644 --- a/src/sys.h +++ b/src/sys.h @@ -261,6 +261,21 @@ enum : int { enum : okm_long { o_rdonly = 0, o_wronly = 1, o_rdwr = 2, + + // ⚠️ THIS SYSTEM'S VALUES, WHICH ARE NOT THE OTHER ONE'S. A read lock is 1 + // here and 0 there, and a write lock is 3 here and 1 there --- the BSD + // numbering rather than the one the other kernel took. A table copied from + // the sibling implementation would compile, run, and take the wrong kind of + // lock. + lock_read = 1, lock_unlock = 2, lock_write = 3, + seek_set = 0, + + // The open-file form, which this system has had since 10.10. The holder is + // the open file rather than the process, which is what openkal states. + f_ofd_getlk = 92, f_ofd_setlk = 90, f_ofd_setlkw = 91, + + // sysctl: how many processors this machine runs at once. + ctl_hw = 6, hw_ncpu = 3, o_creat = 0x0200, o_excl = 0x0800, o_trunc = 0x0400, o_append = 0x0008, o_directory = 0x100000, o_cloexec = 0x1000000, o_nofollow = 0x0100, at_fdcwd = -2, at_removedir = 0x0080, at_symlink_nofollow = 0x0020, @@ -308,6 +323,17 @@ inline int translate(okm_long r) { // consults --- a property that varies between the RESOURCES of an interface is // answered by an enquiry taking the resource, and here the resource's format is // what the enquiry has to look at. +// ⚠️ THIS SYSTEM'S `struct flock' PUTS THE POSITIONS FIRST, and the other +// kernel's puts the kinds first. The two layouts are not interchangeable, and a +// structure copied across would place a 64-bit offset where two shorts belong. +struct kflock { + okm_i64 l_start; + okm_i64 l_len; + int l_pid; + short l_type; + short l_whence; +}; + struct kstatfs64 { okm_u32 f_bsize; okm_u32 f_iosize; // int32; the pair fills the first eight bytes diff --git a/src/task.cpp b/src/task.cpp index b090b33..d6aeeca 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -170,6 +170,24 @@ int kal_task_wait(const kal_u32* word, kal_u32 expected, } } +// How many contexts can run at the same moment. Version 0.10. +// +// ⚠️ Added because its absence was a WRONG ANSWER and not a refusal: +// `KAL_TASK_PROP_PARALLEL' says whether and not how many, so a C library above +// answered 1 with no error and a program sizing a pool of workers got one. +kal_uintptr kal_task_parallelism(void) { + int mib[2] = { okm::ctl_hw, okm::hw_ncpu }; + int value = 0; + okm_uptr length = sizeof value; + const okm_long r = okm::sys(okm::nr_sysctl, + reinterpret_cast(mib), 2, + reinterpret_cast(&value), + reinterpret_cast(&length), 0, 0); + // Zero is "cannot say", and openkal distinguishes it from one on purpose. + if (okm::failed(r) || value <= 0) return 0; + return static_cast(value); +} + int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { if (count == 0) { if (woken) *woken = 0; return kal_ok; } okm_long operation = okm::ul_compare_and_wait | okm::ulf_no_errno; From b9712e077300e6ba9331e484f842e486919583d4 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 17:06:35 +0800 Subject: [PATCH 2/2] A range another holder has is reported as one value, not either of two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ THE STANDARD THIS CALL COMES FROM NAMES TWO VALUES FOR ONE CONDITION AND LEAVES THE CHOICE TO THE SYSTEM. openkal names one. For a lock attempt that does not wait, a range another holder has is reported as EITHER of two errors --- the system decides. So a program written against the standard accepts both, and an implementation of openkal must NOT pass that choice on: `kal_err_again' is the answer a caller polls upon, and the other value translates to `kal_err_permission', which a caller reads as "asking again will not help" and acts upon by stopping. ⭐ NARROWED TO THE ATTEMPT THAT DOES NOT WAIT, because that is the only path for which the two values carry this meaning. A permission failure anywhere else keeps its own answer. ⚠️ Found by looking rather than by failing: this kernel answers the first value in practice, so the observation passes here either way. The sibling implementation is on a system whose lineage answers the other one, and the two implementations must not disagree about what a caller sees. --- src/fs.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/fs.cpp b/src/fs.cpp index d005cef..d731f03 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -332,7 +332,17 @@ static int lock_range(kal_file f, kal_u64 start, kal_u64 len, do { r = okm::sys(okm::nr_fcntl, fd, cmd, reinterpret_cast(&fl)); } while (r == -okm::e_intr); - return okm::failed(r) ? okm::translate(r) : kal_ok; + if (!okm::failed(r)) return kal_ok; + + // ⚠️⚠️ TWO VALUES MEAN ONE THING HERE, AND openkal NAMES ONE OF THEM. The + // standard this call comes from reports a range another holder has as + // EITHER of two values and leaves the choice to the system. openkal says + // `kal_err_again', which a caller polls upon; the other translates to + // `permission', which a caller reads as "asking again will not help". + // Narrowed to the attempt that does not wait, where alone the two carry + // this meaning. + if (!wait && (-r == okm::e_acces || -r == okm::e_again)) return kal_err_again; + return okm::translate(r); } int kal_fs_lock(kal_file f, kal_u64 start, kal_u64 len, kal_uintptr mode) {