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/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 diff --git a/src/fs.cpp b/src/fs.cpp index 0b0a816..5cf0794 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; @@ -563,10 +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_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME + | lockable | 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.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/win.h b/src/win.h index 9e05382..e224f97 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); } @@ -224,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 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,