From 0922007a30544080567b5876799c2c0426b159f7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 16:23:09 +0800 Subject: [PATCH 1/4] 0.5.0 --- openkal 0.10's five operations, four implemented and one recorded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⭐ THIS SYSTEM EXCLUDES PER HANDLE, WHICH IS WHAT openkal STATES. The other two kernels carry an older record lock held by the PROCESS --- released as soon as that process closes any descriptor for the node --- and have to reach past it to the open-file form. Here there is nothing to reach past: `NtLockFile' is the handle's. ⚠️ AND THIS SYSTEM'S EXCLUSION IS MANDATORY RATHER THAN ADVISORY: a write that crosses a locked range is refused by the system, where elsewhere it is refused only to a program that asked. That is a difference a caller can observe and it is the environment's own; simulating the weaker one is not this implementation's to do. `kal_fs_capacity' reports the units this CALLER may use rather than the volume's free total --- a quota makes the two differ, and the larger is not a number a program can act upon. `kal_fs_set_modified_at' does NOT open through `kal_fs_open': that one names `FILE_NON_DIRECTORY_FILE', correctly, since it opens a file --- and a directory is exactly what this declaration exists to reach. It opens for the attribute alone, so a caller need not be able to write the contents to stamp them. `kal_task_parallelism' counts the ACTIVE MASK and not the count field. This record carries both and they differ whenever a program is confined to part of the machine, which is the case a program sizing itself most needs to get right. --- and the one that is not claimed ----------------------------------------- ⚠️⚠️ `kal_process_spawn_bound' IS REFUSED, AND NOT BECAUSE THIS SYSTEM CANNOT. It can: a job object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE' ends every program in the job when the last handle closes, which this system does when a process dies however it dies. That is exactly the binding openkal describes. It is not claimed because it has not been MEASURED here. The one consumer that needs it composes `execve', and this system already declines `openkal.space', so nothing on this system reaches the operation today --- and claiming a binding that has never been exercised is the shape of answer openkal exists to refuse. It is recorded as the next thing this implementation should do rather than as an absence, and a caller that asks `kal_process_props' first is told. --- README.md | 2 +- mcpp.toml | 4 +- src/fs.cpp | 97 ++++++++++++++++++++++++++++++++++++++++++++++++- src/process.cpp | 22 +++++++++++ src/task.cpp | 21 +++++++++++ src/win.h | 21 +++++++++++ 6 files changed, 163 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b3bb09a..604cb98 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. openkal = "0.9.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.4.0" +openkal-windows = "0.5.0" ``` Its purpose is as much to test the specification as to be used. openkal was diff --git a/mcpp.toml b/mcpp.toml index 4181ba2..42b2591 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.4.0" +version = "0.5.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-windows" [dependencies] -openkal = "0.9.0" +openkal = "0.10.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/src/fs.cpp b/src/fs.cpp index 0b0a816..029d1e1 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -415,6 +415,100 @@ int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { return okw::ok(r) ? kal_ok : okw::translate_nt(r); } +// The modification time of a NAME, including a directory. Version 0.10. +// +// ⚠️ AND THE OPEN IS NOT `kal_fs_open''S. That one names `FILE_NON_DIRECTORY_FILE' +// --- correctly, since it opens a FILE --- and a directory is exactly what this +// declaration exists to reach. Opening for the attribute alone also means a +// caller need not be able to write the contents to stamp them, which is what +// `utimensat' means everywhere else. +int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, + kal_u64 modified_ns) { + void* root = dir_handle(base); + if (!root || !okw::acceptable(name, len)) return kal_err_invalid; + + void* h = nullptr; + const long r = open_relative(root, name, len, FILE_WRITE_ATTRIBUTES, + okw::file_open, + okw::file_open_for_backup_intent, &h); + if (!okw::ok(r)) return okw::translate_nt(r); + + okw::file_basic_information basic{}; + basic.last_write_time = + static_cast(modified_ns / 100ull + kEpochDifference); + okw::io_status_block iosb{}; + const long w = okw::NtSetInformationFile(h, &iosb, &basic, sizeof basic, + okw::file_basic_information_class); + okw::NtClose(h); + return okw::ok(w) ? kal_ok : okw::translate_nt(w); +} + +// --- exclusion upon a range of a file --------------------------------------- +// +// ⭐ THIS SYSTEM EXCLUDES PER HANDLE, WHICH IS WHAT openkal STATES. The other +// two kernels carry an older form held by the PROCESS and have to reach past it; +// here there is nothing to reach past. +// +// ⚠️ AND THIS SYSTEM'S EXCLUSION IS MANDATORY RATHER THAN ADVISORY: a write that +// crosses a locked range is refused by the system, where elsewhere it is refused +// only to a program that asked. That is a difference a caller can observe, and +// it is the environment's own; nothing here can or should simulate the weaker +// one. +static int lock_range(kal_file f, kal_u64 start, kal_u64 len, + bool exclusive, bool wait, bool release) { + void* h = file_handle(f); + if (!h) return kal_err_invalid; + + okw_i64 offset = static_cast(start); + // openkal spells "to the end, however far that comes to be" as zero; this + // system has no such spelling and takes a count, so the largest one stands + // for it --- which is what every C library on this system does for the same + // reason. + okw_i64 length = len ? static_cast(len) + : static_cast(0x7fffffffffffffffll); + + okw::io_status_block iosb{}; + const long r = release + ? okw::NtUnlockFile(h, &iosb, &offset, &length, 0) + : okw::NtLockFile(h, nullptr, nullptr, nullptr, &iosb, &offset, &length, + 0, static_cast(wait ? 0 : 1), + static_cast(exclusive ? 1 : 0)); + return okw::ok(r) ? kal_ok : okw::translate_nt(r); +} + +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, exclusive, (mode & KAL_LOCK_WAIT) != 0, false); +} + +int kal_fs_unlock(kal_file f, kal_u64 start, kal_u64 len) { + return lock_range(f, start, len, false, false, true); +} + +// How much the volume holds, in bytes. +// +// ⚠️ `available' AND NOT `total free'. This system reports the units this +// CALLER may use, which is the question openkal asks; a quota makes the two +// differ and the larger of them is not an answer a program can act upon. +int kal_fs_capacity(kal_dir d, kal_u64* total, kal_u64* available) { + void* h = dir_handle(d); + if (!h) return kal_err_invalid; + + okw::io_status_block s{}; + okw::file_fs_size_information info{}; + const long r = okw::NtQueryVolumeInformationFile(h, &s, &info, sizeof info, + okw::fs_size_information_class); + if (!okw::ok(r)) return okw::translate_nt(r); + + const kal_u64 unit = static_cast(info.sectors_per_unit) + * static_cast(info.bytes_per_sector); + if (total) *total = static_cast(info.total_allocation_units) * unit; + if (available) *available = static_cast(info.available_allocation_units) * unit; + return kal_ok; +} + int kal_fs_mkdir(kal_dir base, const char* name, kal_uintptr len) { void* root = dir_handle(base); if (!root || !okw::acceptable(name, len)) return kal_err_invalid; @@ -566,7 +660,8 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, kal_uintptr kal_fs_props(kal_dir d) { void* h = dir_handle(d); 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 (!h) return 0; okw::io_status_block s{}; diff --git a/src/process.cpp b/src/process.cpp index c2404db..039f7ea 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -290,6 +290,28 @@ void kal_process_close(kal_process p) { // KAL_PROCESS_PROP_GRANT_DIR is deliberately absent: kal_process_spawn_with // refuses a non-empty set of grants here, and a word claiming a facility the // next call refuses is the disagreement clause 6.2 exists to prevent. +// Starting a program whose lifetime is bound to this one's. Version 0.10. +// +// ⚠️⚠️ REFUSED HERE, AND NOT BECAUSE THIS SYSTEM CANNOT --- IT CAN. A job object +// with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE' ends every program in the job when +// the last handle to it closes, which this system does when a process dies +// however it dies. That is exactly the binding openkal describes. +// +// ⚠️ IT IS NOT CLAIMED IN THIS RELEASE BECAUSE IT HAS NOT BEEN MEASURED HERE. +// The one consumer that needs it composes `execve', and this system already +// declines `openkal.space' --- so nothing on this system reaches the operation +// today, and claiming a binding that has never been exercised is the shape of +// answer openkal exists to refuse. It is the next thing this implementation +// should do, and it is recorded as that rather than as an absence. +// +// A caller that asks `kal_process_props' 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/task.cpp b/src/task.cpp index 3873088..0f8de64 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -102,6 +102,27 @@ 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: the +// property word says whether contexts run at once and not how many can, so a C +// library above answered 1 with no error and a program sizing a pool of workers +// got one worker. +// +// ⭐ THE ACTIVE MASK AND NOT THE COUNT FIELD. This record carries both, and they +// differ whenever a program is confined to part of the machine --- which is the +// case a program sizing itself most needs to get right. +kal_uintptr kal_task_parallelism(void) { + SYSTEM_INFO info{}; + GetSystemInfo(&info); + kal_uintptr count = 0; + for (unsigned long long bit = info.dwActiveProcessorMask; bit; bit &= bit - 1) + ++count; + if (count) return count; + // Zero is "cannot say", and openkal distinguishes it from one on purpose. + return static_cast(info.dwNumberOfProcessors); +} + int kal_task_wake(const kal_u32* word, kal_uintptr count, kal_uintptr* woken) { void* address = const_cast(static_cast(word)); if (count == 0) { if (woken) *woken = 0; return kal_ok; } diff --git a/src/win.h b/src/win.h index 9e05382..8a4b186 100644 --- a/src/win.h +++ b/src/win.h @@ -154,6 +154,17 @@ enum : int { enum : int { fs_volume_information_class = 1, fs_attribute_information_class = 5, + fs_size_information_class = 3, +}; + +// How much the volume holds. The counts are in allocation units and the record +// says how many bytes one is, which is why capacity is two multiplications and +// not a field. +struct file_fs_size_information { + okw_i64 total_allocation_units; + okw_i64 available_allocation_units; + unsigned long sectors_per_unit; + unsigned long bytes_per_sector; }; // The dispositions NtCreateFile takes. They are the whole of what @@ -210,6 +221,16 @@ __declspec(dllimport) long __stdcall NtQueryDirectoryFile(void* handle, void* ev int cls, unsigned char single, unicode_string* pattern, unsigned char restart); __declspec(dllimport) long __stdcall NtFlushBuffersFile(void* handle, io_status_block* status); + +// ⭐ EXCLUSION IS PER-HANDLE ON THIS SYSTEM, which is exactly what openkal +// states: the holder is the `kal_file'. There is no second, process-held form +// to avoid here --- the thing the other two kernels have to reach past. +__declspec(dllimport) long __stdcall NtLockFile(void* handle, void* event, void* apc, void* apc_context, + io_status_block* status, okw_i64* offset, okw_i64* length, + unsigned long key, unsigned char fail_immediately, + unsigned char exclusive); +__declspec(dllimport) long __stdcall NtUnlockFile(void* handle, io_status_block* status, + okw_i64* offset, okw_i64* length, unsigned long key); __declspec(dllimport) unsigned long __stdcall RtlNtStatusToDosError(long status); } From bdd0794b3879f4f09cf6b6319c4701f33f520fcf Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 16:55:59 +0800 Subject: [PATCH 2/4] The two lock imports are declared, so they are exported by a .def MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⭐ CAUGHT BY THIS PACKAGE'S OWN CHECK, WHICH EXISTS FOR EXACTLY THIS. declared in src/*.h and exported by no .def: NtLockFile NtUnlockFile A declaration in `src/win.h' and an entry in `port/*.def' are two statements of one fact --- what this package imports from the object manager --- and the second is what an import library is built from. A declaration without the entry compiles and does not link, and the check turns that into a line naming the two names rather than a linker message naming a symbol. ⚠️ It is the same shape as the source-exclusion list in the sibling C library: a second statement of one fact falls behind the first, and the thing that catches it is a check that compares them. --- port/ntdll.def | 2 ++ 1 file changed, 2 insertions(+) diff --git a/port/ntdll.def b/port/ntdll.def index 7078316..09750ec 100644 --- a/port/ntdll.def +++ b/port/ntdll.def @@ -8,10 +8,12 @@ EXPORTS NtClose NtCreateFile NtFlushBuffersFile +NtLockFile NtQueryDirectoryFile NtQueryInformationFile NtQueryVolumeInformationFile NtReadFile NtSetInformationFile +NtUnlockFile NtWriteFile RtlNtStatusToDosError From b9db125f2de0860a86f07aec771549e0f0ef69e8 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 17:02:15 +0800 Subject: [PATCH 3/4] A lock another holder has is 'again', not an input-output failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ THE TRANSLATION TABLE HAD NO ENTRY FOR A LOCK CONFLICT, because nothing in this implementation took a lock until openkal 0.10. `kal_fs_lock' without KAL_LOCK_WAIT reports `kal_err_again' where the range is held --- that is the answer a caller POLLS UPON, and openkal says so. Without the entry, `ERROR_LOCK_VIOLATION' fell to the default arm and became `kal_err_io': a failure of the DEVICE rather than a conflict with another holder. A caller reading that stops; a caller reading `again' retries. ⭐ IT IS DISTINCT FROM `ERROR_SHARING_VIOLATION', WHICH STAYS `permission'. That one is a conflict over how a file was OPENED and is not resolved by asking again. Two errors that look adjacent and mean opposite things for a caller. Found by the specification's own conformance suite, on the observation added with the operation --- which is the argument for adding the observation with the operation. --- src/win.cpp | 14 ++++++++++++++ src/win32.h | 1 + 2 files changed, 15 insertions(+) diff --git a/src/win.cpp b/src/win.cpp index c5576ca..047c836 100644 --- a/src/win.cpp +++ b/src/win.cpp @@ -55,6 +55,20 @@ int translate_win32(unsigned long e) { case ERROR_ALREADY_EXISTS: return kal_err_exists; case ERROR_DIR_NOT_EMPTY: return kal_err_not_empty; case ERROR_DIRECTORY: return kal_err_not_directory; + // ⚠️⚠️ A LOCK THAT ANOTHER HOLDER HAS IS `AGAIN' AND NOT AN + // INPUT-OUTPUT FAILURE, and this line is missing from every earlier + // release because nothing here took a lock until openkal 0.10. + // + // `kal_fs_lock' without KAL_LOCK_WAIT reports kal_err_again where the + // range is held, which is the answer a caller POLLS UPON. Falling to + // the arm below would have reported kal_err_io --- a failure of the + // device rather than a conflict with another holder --- and a caller + // reading that would stop rather than retry. + // + // ⭐ It is distinct from ERROR_SHARING_VIOLATION above, which stays + // `permission': that one is a conflict over how a file was OPENED and + // is not resolved by asking again. + case ERROR_LOCK_VIOLATION: return kal_err_again; case ERROR_IO_PENDING: return kal_err_again; default: return kal_err_io; } diff --git a/src/win32.h b/src/win32.h index 818bd68..0deac28 100644 --- a/src/win32.h +++ b/src/win32.h @@ -216,6 +216,7 @@ enum : DWORD { ERROR_OUTOFMEMORY = 14, ERROR_WRITE_PROTECT = 19, ERROR_SHARING_VIOLATION = 32, + ERROR_LOCK_VIOLATION = 33, ERROR_HANDLE_EOF = 38, ERROR_HANDLE_DISK_FULL = 39, ERROR_NOT_SUPPORTED = 50, From 46bb425d17ae1e2ebf4735b2c096d7e2f949d6c7 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 30 Aug 2026 17:21:47 +0800 Subject: [PATCH 4/4] The lock position is asked about rather than assumed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ AN ENVIRONMENT MAY EXPORT A NAME AND NOT IMPLEMENT WHAT IT NAMES. This system locks a byte range, and the three continuous-integration rows that run ON it measure that it does. A fourth row cross-builds and runs the result under an EMULATOR of this system, which exports `NtLockFile' and answers `STATUS_NOT_IMPLEMENTED' when it is called. ⭐ SO THE PROPERTY IS NOT A PROPERTY OF THE VOLUME, NOR OF THE FORMAT. It is a property of what is beneath the program at the moment it asks. A word that claimed the position regardless would be describing the INTERFACE rather than the environment --- and the whole purpose of a capability word is that a caller may ask before it calls and be told the truth about where it is. ⚠️ THE ENQUIRY IS MADE ON A DIRECTORY, WHICH IS NOT A THING THIS SYSTEM LOCKS, and that is what makes it answerable without disturbing anything. A system that implements the operation refuses a directory as a wrong request; one that has not implemented it says so with a different value, and that difference is the whole of the question. Nothing is locked either way, and it is asked once. Measured under that emulator, through the C library above: a lock now reports `ENOSYS' --- which is true there --- rather than the suite reporting four observations that did not hold. On the three rows that run on the system itself, nothing changes and the observations hold. --- src/fs.cpp | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- src/win.h | 7 +++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/fs.cpp b/src/fs.cpp index 029d1e1..5cf0794 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -657,11 +657,59 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, // guessed: names on the volume this system is ordinarily installed on are // compared without regard to case, and a volume attached to the same machine // may be otherwise --- and a word per implementation could state neither. +// Whether the environment beneath actually performs a lock. +// +// ⚠️ ASKED ON A DIRECTORY, WHICH IS NOT A THING THIS SYSTEM LOCKS --- and that is +// what makes the question answerable without disturbing anything. A system that +// implements the operation refuses a directory as a wrong request; one that has +// not implemented it says so with a different value, and that difference is the +// whole of the enquiry. Nothing is locked either way. +// +// Answered once. It is a property of what is beneath this program rather than of +// a volume, so it does not vary between the directories one program holds. +static bool locking_available() { + static int cached = -1; + if (cached >= 0) return cached != 0; + const kal_uintptr count = kal_fs_preopen_count(); + cached = 1; + if (count > 0) { + kal_dir probe{}; + char name[8]; kal_uintptr len = 0; + if (kal_fs_preopen(0, &probe, name, sizeof name, &len) == kal_ok) { + void* h = dir_handle(probe); + if (h) { + okw::io_status_block iosb{}; + okw_i64 off = 0, len2 = 1; + const long r = okw::NtLockFile(h, nullptr, nullptr, nullptr, &iosb, + &off, &len2, 0, 1, 1); + if (okw::ok(r)) okw::NtUnlockFile(h, &iosb, &off, &len2, 0); + else if (r == okw::status_not_implemented) cached = 0; + } + } + } + return cached != 0; +} + kal_uintptr kal_fs_props(kal_dir d) { void* h = dir_handle(d); + // ⚠️⚠️ LOCKING IS ASKED ABOUT RATHER THAN ASSUMED, AND THE REASON IS NOT + // THE VOLUME. + // + // This system locks a byte range, and the three continuous-integration rows + // that run on it measure that it does. A FOURTH row cross-builds and runs + // the result under an emulator of this system --- which EXPORTS the call and + // answers `STATUS_NOT_IMPLEMENTED' when it is made. + // + // ⭐ So the property is not a property of the volume here, nor of the + // format: it is a property of what is beneath the program at the moment it + // asks. A word that claimed the position regardless would be describing the + // INTERFACE rather than the environment --- and the whole purpose of a + // capability word is that a caller may ask before it calls and be told the + // truth about where it is. + const kal_uintptr lockable = locking_available() ? KAL_FS_PROP_LOCKS : 0; const kal_uintptr conservative = KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME - | KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY; + | lockable | KAL_FS_PROP_CAPACITY; if (!h) return 0; okw::io_status_block s{}; diff --git a/src/win.h b/src/win.h index 8a4b186..e224f97 100644 --- a/src/win.h +++ b/src/win.h @@ -245,6 +245,13 @@ inline bool ok(long status) { return status >= 0; } // reported to have the same identity. inline constexpr long status_buffer_overflow = static_cast(0x80000005ul); +// ⚠️ AN ENVIRONMENT MAY EXPORT A NAME AND NOT IMPLEMENT WHAT IT NAMES, and the +// two are distinguishable only by this value. It is what a capability word has +// to consult before claiming a position: an operation whose export resolves and +// whose call answers this cannot be performed here, and a word claiming it would +// be describing the interface rather than the environment. +inline constexpr long status_not_implemented = static_cast(0xC0000002ul); + // --- translation ------------------------------------------------------------- // // The environment's error values are mapped onto the closed set the