diff --git a/README.md b/README.md index 5f6b77c..de4178d 100644 --- a/README.md +++ b/README.md @@ -77,16 +77,16 @@ conditional on the target. ```toml [dependencies] -openkal = "0.9.0" +openkal = "0.10.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.7.1" +openkal-linux = "0.9.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.6.1" +openkal-macos = "0.7.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.4.0" +openkal-windows = "0.5.0" ``` The program imports the interface and names no implementation. diff --git a/SPEC.md b/SPEC.md index 1ad6915..4eee725 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.9 +# openkal Specification, version 0.10 ## 1. Scope @@ -50,7 +50,7 @@ provides an interface in whole or not at all. | `openkal.timeout` | a bound upon operations that would otherwise wait | optional | ✓ | ✓ | ✓ | | `openkal.event` | readiness of a set of resources | reserved | | | | -Version 0.9 specifies the core and optional interfaces. The reserved row is not +Version 0.10 specifies the core and optional interfaces. 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 @@ -1033,3 +1033,72 @@ The following are recorded so that they are not mistaken for oversights. general mechanism for passing a handle to a context in another space is not defined by this version. It is the question `openkal.space` reaches first and is not peculiar to it. +10. **Exclusion upon a range of a file.** ⚠️ **Settled in 0.10.** `kal_fs_lock` + and `kal_fs_unlock` are operations of `openkal.fs`, admitted on exactly the + grounds entry 7 records for links: whether a *volume* can exclude is a + property of the format rather than of the environment, so `kal_fs_props` + answers it and a caller asks before it calls. + + ⭐ **And this one is the opposite of entry 6, which is why the two are next + to each other.** Permission was declined because the environments do not + agree that an identity exists. Every environment this specification targets + locks a byte range, and spells it almost identically. What was missing was a + word, not a capability. + + ⚠️ **What its absence cost, and it was not a refusal.** A C library above + this interface answers `fcntl(F_SETLK)`. With nothing here to answer it + with, one returned success and took no lock — **two programs held one + exclusive lock and neither could find out**. Measured against a host. This + entry exists so that "a specification with no operation for X" is not + mistaken for "consumers of it will simply not do X". + + ⭐ **Release upon the holder's end is required rather than observed**, and + that requirement is the whole reason the operation cannot be composed above + the line: a caller can build exclusion out of `KAL_OPEN_EXCLUSIVE` and a + name, and nothing then releases that name when its holder dies. +11. **A started program that does not outlive its caller.** ⚠️ **Settled in + 0.10.** `kal_process_spawn_bound`. + + Clause 7.1 declines to replace a running image, and that stands. The + consequence, which this entry did not previously record, is that a C library + asked for `execve` composes it — and the composition leaves **three** images + where a system with the operation has two: the caller, a copy that waits, + and the program. + + ⚠️ **A signal reaches the middle one.** `kal_process_terminate` upon the + identifier the caller holds terminates the waiter; measured with a host as + control, the caller is told the program died on the signal it sent while the + program runs to completion, unsupervised. The termination operation was not + at fault — it was asked to terminate one started program and did. What was + missing was a way to *say* the thing `execve` means. +12. **How many contexts run at once.** ⚠️ **Settled in 0.10.** + `kal_task_parallelism`. `KAL_TASK_PROP_PARALLEL` says *whether* and not *how + many*, and a C library above had nowhere else to look: `hardware_concurrency` + answered 1 with no error, so a program sizing a pool of workers got one + worker and no way to know. ⭐ Zero means "cannot say" and is distinct from + one, because an environment with one processor and an environment that will + not answer call for different behaviour. +13. **The time of a name, and the size of a volume.** ⚠️ **Settled in 0.10.** + `kal_fs_set_modified_at` and `kal_fs_capacity`. The first exists because + `kal_fs_set_modified` is stated on an open *file* while a directory is + opened as a `kal_dir` — so this interface had no route to a directory's time + at all, and a consumer that stamps a lock directory drove one implementation + to open the directory for *reading* and set the time on that, outside + anything stated here. A divergence caused by a missing declaration is a + defect of the specification and is recorded as one. + + The second, `kal_fs_capacity`, exists for the reason that keeps it in this + interface rather than in `openkal.space`: how much room a volume has is a + property of the *names* a caller can already reach, not of the memory a + program runs in. A C library above answers `statvfs`, and with nothing here + to answer it with it reported a fixed number — ⚠️ **which is worse than + refusing, because a program that checks for room before writing was told + there was room.** + + ⭐ It answers bytes and not blocks, and that is the whole of the design + decision. Every environment this specification targets states a block count + and a block size, in units of its own choosing, and every one of them + differs; a caller wanting bytes multiplies two numbers whose meaning it must + first look up. Bytes are what the caller is deciding about, so bytes are + what this returns and the multiplication happens once, in the + implementation, where the units are known. diff --git a/SURFACE.txt b/SURFACE.txt index ad7e660..10048cb 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.9, one name per line. +# The C surface of openkal 0.10, 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 @@ -50,6 +50,7 @@ kal_time_wall kal_random_fill kal_random_props # openkal.fs +kal_fs_capacity kal_fs_close_dir kal_fs_close_file kal_fs_file_info @@ -58,6 +59,7 @@ kal_fs_link_create kal_fs_link_read kal_fs_list_begin kal_fs_list_next +kal_fs_lock kal_fs_max_name kal_fs_mkdir kal_fs_open @@ -69,20 +71,24 @@ kal_fs_remove kal_fs_rename kal_fs_seek kal_fs_set_modified +kal_fs_set_modified_at kal_fs_stream kal_fs_truncate +kal_fs_unlock # openkal.process kal_process_channel kal_process_channel_close kal_process_close kal_process_props kal_process_spawn +kal_process_spawn_bound kal_process_spawn_with kal_process_terminate kal_process_wait # openkal.task kal_task_current kal_task_join +kal_task_parallelism kal_task_props kal_task_start kal_task_wait diff --git a/conformance/mcpp.toml b/conformance/mcpp.toml index ecd9abe..5fea0c9 100644 --- a/conformance/mcpp.toml +++ b/conformance/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-conformance" -version = "0.7.0" +version = "0.8.0" description = "The behavioural half of clause 9: a suite an implementation of openkal runs against itself, selectable to the interfaces it provides." license = "Apache-2.0" authors = ["mcpplibs"] @@ -18,7 +18,7 @@ repo = "https://github.com/mcpplibs/openkal" # What it depends upon is therefore openkal and the language. The formatting in # okc.report is the price of that, and it is sixty lines. [dependencies] -openkal = "0.9.0" +openkal = "0.10.0" # The implementation under examination is not named here. # diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index b3e8c62..624d1c1 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -46,6 +46,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_exec_publish); (void)sizeof(&kal_exit); (void)sizeof(&kal_free); + (void)sizeof(&kal_fs_capacity); (void)sizeof(&kal_fs_close_dir); (void)sizeof(&kal_fs_close_file); (void)sizeof(&kal_fs_file_info); @@ -54,6 +55,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_link_read); (void)sizeof(&kal_fs_list_begin); (void)sizeof(&kal_fs_list_next); + (void)sizeof(&kal_fs_lock); (void)sizeof(&kal_fs_max_name); (void)sizeof(&kal_fs_mkdir); (void)sizeof(&kal_fs_open); @@ -65,8 +67,10 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_rename); (void)sizeof(&kal_fs_seek); (void)sizeof(&kal_fs_set_modified); + (void)sizeof(&kal_fs_set_modified_at); (void)sizeof(&kal_fs_stream); (void)sizeof(&kal_fs_truncate); + (void)sizeof(&kal_fs_unlock); (void)sizeof(&kal_interfaces); (void)sizeof(&kal_memory_granularity); (void)sizeof(&kal_net_accept); @@ -85,6 +89,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_process_close); (void)sizeof(&kal_process_props); (void)sizeof(&kal_process_spawn); + (void)sizeof(&kal_process_spawn_bound); (void)sizeof(&kal_process_spawn_with); (void)sizeof(&kal_process_terminate); (void)sizeof(&kal_process_wait); @@ -101,6 +106,7 @@ void okc_declarations_c(void) (void)sizeof(&kal_stream_write); (void)sizeof(&kal_task_current); (void)sizeof(&kal_task_join); + (void)sizeof(&kal_task_parallelism); (void)sizeof(&kal_task_props); (void)sizeof(&kal_task_start); (void)sizeof(&kal_task_wait); diff --git a/conformance/src/report.cpp b/conformance/src/report.cpp index 5b43d5a..a00d7a0 100644 --- a/conformance/src/report.cpp +++ b/conformance/src/report.cpp @@ -83,7 +83,7 @@ int failed_count() { return g_failed; } int unobserved_count() { return g_unobserved; } void write_inventory() { - line("openkal conformance suite, version 0.9.0"); + line("openkal conformance suite, version 0.10.0"); line(""); line("interface provision examined select with"); for (const auto& row : inventory) { diff --git a/conformance/src/sections/fs.cpp b/conformance/src/sections/fs.cpp index 30ffb44..fad1e21 100644 --- a/conformance/src/sections/fs.cpp +++ b/conformance/src/sections/fs.cpp @@ -478,13 +478,119 @@ void run() { } } + // --- exclusion upon a range of a file, version 0.10 --------------------- + // + // ⭐⭐ THE OBSERVATION THAT TELLS THE TWO FORMS OF THIS APART NEEDS NO SECOND + // PROGRAM, AND THAT IS WHY IT IS WRITTEN THIS WAY. + // + // openkal states the holder as the open FILE. One environment's oldest form + // holds it by the PROCESS and releases every lock upon a node as soon as the + // program closes any descriptor for it --- so a library that opened one file + // twice destroyed its own lock. An implementation built on that form passes + // "a lock can be taken" and "it can be released" and fails only here: a + // SECOND OPEN FILE of one name, in this program, must be refused. + if (performs(kind::behaviour)) { + if ((kal_fs_props(here()) & kal::fs::locks.bits) != 0) { + put_file(kName, "xxxx"); + kal_file a{}, b{}; + const int oa = kal::fs::open_file(here(), kName, length(kName), + kal::fs::open::read | kal::fs::open::write, &a); + const int taken = oa == kal_ok + ? kal::fs::lock_range(a, 0, 0, kal::fs::lock::exclusive) : oa; + observe(kind::behaviour, taken == kal_ok, + "an exclusive lock upon a whole file is taken"); + + const int ob = kal::fs::open_file(here(), kName, length(kName), + kal::fs::open::read | kal::fs::open::write, &b); + const int second = ob == kal_ok + ? kal::fs::lock_range(b, 0, 0, kal::fs::lock::exclusive) : ob; + observe(kind::behaviour, second == kal_err_again, + "and a second open file of the same name is refused, not granted"); + + const int freed = taken == kal_ok ? kal_fs_unlock(a, 0, 0) : kal_err_invalid; + observe(kind::behaviour, freed == kal_ok, "the lock is released"); + const int again = ob == kal_ok + ? kal::fs::lock_range(b, 0, 0, kal::fs::lock::exclusive) : ob; + observe(kind::behaviour, again == kal_ok, + "and once released, another open file may take it"); + if (again == kal_ok) kal_fs_unlock(b, 0, 0); + + // Neither kind and both kinds are the same mistake: a caller that + // asked for neither did not say what it wanted. + observe(kind::behaviour, + oa != kal_ok || kal_fs_lock(a, 0, 0, 0) == kal_err_invalid, + "asking for a lock that is neither shared nor exclusive is refused"); + + if (ob == kal_ok) kal_fs_close_file(b); + if (oa == kal_ok) kal_fs_close_file(a); + kal_fs_remove(here(), kName, length(kName)); + } else { + unobserved(kind::behaviour, + "an exclusive lock upon a whole file is taken", + "the implementation does not claim prop_locks for this volume"); + } + + // --- how much the volume holds -------------------------------------- + if ((kal_fs_props(here()) & kal::fs::capacity.bits) != 0) { + kal_u64 total = 0, available = 0; + const int e = kal_fs_capacity(here(), &total, &available); + observe(kind::behaviour, e == kal_ok && total > 0, + "the volume reports how much it holds"); + observe(kind::behaviour, e == kal_ok && available <= total, + "and what is available is no more than that"); + // Either pointer may be null, for a caller that wants one of the two. + kal_u64 one = 0; + observe(kind::behaviour, + kal_fs_capacity(here(), nullptr, &one) == kal_ok, + "and a caller may ask for one of the two"); + } else { + unobserved(kind::behaviour, "the volume reports how much it holds", + "the implementation does not claim prop_capacity for this volume"); + } + + // --- the modification time of a NAME, including a directory ---------- + // + // ⭐ THE DIRECTORY IS THE POINT. `kal_fs_set_modified' takes a `kal_file' + // and a directory is a `kal_dir', so before this declaration there was no + // route to a directory's time at all --- and an implementation reached one + // anyway, outside anything this specification stated. + if ((kal_fs_props(here()) & kal::fs::modified_time.bits) != 0) { + kal_fs_mkdir(here(), kDir, length(kDir)); + const kal_u64 chosen = 1600000000ull * 1000000000ull; + const int e = kal_fs_set_modified_at(here(), kDir, length(kDir), chosen); + kal_node_info after = fresh(); + const int read_back = + kal_fs_info(here(), kDir, length(kDir), 0, kal::fs::field::all, &after); + observe(kind::behaviour, + e == kal_ok && read_back == kal_ok + && after.modified_ns / 1000000000u == chosen / 1000000000u, + "the time a DIRECTORY reports as its last modification is set by name"); + kal_fs_remove(here(), kDir, length(kDir)); + + put_file(kName, "x"); + const int fe = kal_fs_set_modified_at(here(), kName, length(kName), chosen); + kal_node_info fa = fresh(); + const int fr = kal_fs_info(here(), kName, length(kName), 0, kal::fs::field::all, &fa); + observe(kind::behaviour, + fe == kal_ok && fr == kal_ok + && fa.modified_ns / 1000000000u == chosen / 1000000000u, + "and so is a file's, by the same operation"); + kal_fs_remove(here(), kName, length(kName)); + } else { + unobserved(kind::behaviour, + "the time a DIRECTORY reports as its last modification is set by name", + "the implementation does not claim prop_modified_time"); + } + } + if (performs(kind::abi)) { observe(kind::abi, sizeof(kal_dir) == sizeof(kal_uintptr) && sizeof(kal_file) == sizeof(kal_uintptr), "a directory and a file handle each occupy one machine word"); const kal_uintptr assigned = (kal::fs::case_sensitive | kal::fs::links | kal::fs::modified_time | kal::fs::atomic_rename - | kal::fs::make_links).bits; + | kal::fs::make_links + | kal::fs::locks | kal::fs::capacity).bits; observe(kind::abi, (kal_fs_props(here()) & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 14906ae..4694997 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -142,11 +142,31 @@ void run() { "the stream selection occupies three machine words"); const kal_uintptr assigned = (kal::process::terminate | kal::process::stream_passing | kal::process::exit_status | kal::process::channel - | kal::process::grant_dir).bits; + | kal::process::grant_dir + | kal::process::bound_lifetime).bits; observe(kind::abi, sizeof(kal_preopen) == 3 * sizeof(kal_uintptr), "a directory grant occupies three machine words"); observe(kind::abi, (kal_process_props() & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); + + // ⚠️ AN OPERATION THAT IS NOT CLAIMED SHALL REFUSE RATHER THAN PERFORM + // SOMETHING ELSE. A caller that asks for a bound lifetime asked for it; + // a program started WITHOUT the binding is not the program it asked to + // start, and an implementation that quietly starts one anyway is the + // failure the operation exists to remove. + if ((kal_process_props() & kal::process::bound_lifetime.bits) == 0) { + kal_process p{}; + const char* argv[1] = { "x" }; + const kal_uintptr lens[1] = { 1 }; + const int e = kal_process_spawn_bound(kal::fs::working(), "x", 1, argv, lens, 1, + nullptr, nullptr, 0, nullptr, &p); + observe(kind::behaviour, e == kal_err_not_supported, + "a lifetime this implementation cannot bind is refused, not ignored"); + } else { + unobserved(kind::behaviour, + "a lifetime this implementation cannot bind is refused, not ignored", + "the implementation claims prop_bound_lifetime"); + } } if (performs(kind::stability)) { diff --git a/conformance/src/sections/task.cpp b/conformance/src/sections/task.cpp index c21984f..143c1f1 100644 --- a/conformance/src/sections/task.cpp +++ b/conformance/src/sections/task.cpp @@ -220,6 +220,17 @@ void run() { | kal::task::thread_local_storage).bits; observe(kind::abi, (kal_task_props() & ~assigned) == 0, "the capability word contains no position the specification has not assigned"); + // ⭐ ZERO IS `CANNOT SAY' AND IS DISTINCT FROM ONE, which is the whole + // reason this enquiry exists: before it, a C library above answered 1 + // with no error and a program sizing a pool of workers got one worker. + // An implementation that will not say must say so, and not say "one". + const kal_uintptr n = kal_task_parallelism(); + observe(kind::behaviour, + (kal_task_props() & kal::task::parallel.bits) == 0 ? n == 1 : true, + "an implementation whose contexts do not run at once reports one"); + observe(kind::behaviour, kal_task_parallelism() == n, + "how many contexts can run at once is the same answer each time"); + observe(kind::abi, kal_task_current() == kal_task_current(), "the identity of the calling context is stable within it"); diff --git a/examples/portable/mcpp.toml b/examples/portable/mcpp.toml index f48fb2c..3968e93 100644 --- a/examples/portable/mcpp.toml +++ b/examples/portable/mcpp.toml @@ -9,13 +9,13 @@ name = "portable" version = "0.1.0" [dependencies] -openkal = "0.9.0" +openkal = "0.10.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.7.1" +openkal-linux = "0.9.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.6.1" +openkal-macos = "0.7.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.4.0" +openkal-windows = "0.5.0" diff --git a/include/openkal/fs.h b/include/openkal/fs.h index 313b54e..760254b 100644 --- a/include/openkal/fs.h +++ b/include/openkal/fs.h @@ -126,6 +126,11 @@ struct kal_node_info { #define KAL_FS_PROP_ATOMIC_RENAME ((kal_uintptr)1u << 3) #define KAL_FS_PROP_MAKE_LINKS ((kal_uintptr)1u << 4) /* kal_fs_link_create * is answered here */ +#define KAL_FS_PROP_LOCKS ((kal_uintptr)1u << 5) /* kal_fs_lock and + * kal_fs_unlock are + * answered here. 0.10 */ +#define KAL_FS_PROP_CAPACITY ((kal_uintptr)1u << 6) /* kal_fs_capacity is + * answered here. 0.10 */ /* Positions in the flags word of kal_fs_open. */ #define KAL_OPEN_READ ((kal_uintptr)1u << 0) @@ -281,6 +286,105 @@ int kal_fs_file_info(struct kal_file, kal_u32 wanted, * this. */ int kal_fs_set_modified(struct kal_file, kal_u64 modified_ns); +/* Sets the time a NAME's node reports. Version 0.10. + * + * ⚠️⚠️ A SECOND DECLARATION BECAUSE THE FIRST CANNOT REACH A DIRECTORY, AND THE + * GAP WAS FOUND BY A CONSUMER RATHER THAN HERE. + * + * The operation above is stated on an open FILE, for a reason that remains + * good. But a directory is opened by `kal_fs_open_dir', which yields a + * `kal_dir', and there is no form of the operation above that takes one --- so + * this interface had no route at all to a directory's time, while `kal_fs_info' + * reports one perfectly well. + * + * ⭐ MEASURED THREE LAYERS UP. A consumer takes a lock by making a directory and + * refreshes the lock by stamping it. Reading the stamp worked; writing it could + * not be expressed, and openkal-musl reached it by opening the directory for + * READING and setting the time on that --- which Linux and macOS perform and + * which this interface does not sanction. That divergence exists because this + * declaration did not. + * + * The name is resolved, as opening resolves. Where the name refers to a FILE the + * effect is that of the operation above; the file-taking form remains for a + * caller that holds one and does not want the name resolved again. + * + * An implementation that does not claim KAL_FS_PROP_MODIFIED_TIME for the volume + * reports kal_err_not_supported. */ +int kal_fs_set_modified_at(struct kal_dir base, const char* name, kal_uintptr len, + kal_u64 modified_ns); + +/* Excluding other holders from a range of a file. Version 0.10. + * + * ⚠️⚠️ ADDED BECAUSE ITS ABSENCE WAS SILENTLY UNSAFE ONE LAYER UP, AND THE + * ADMISSIBILITY ARGUMENT IS THE ONE THE LINK OPERATIONS BELOW ALREADY MAKE. + * + * A C library above this interface answers `fcntl(F_SETLK)'. With nothing here + * to answer it with, openkal-musl returned success and took no lock: two + * programs held one exclusive lock and neither could find out. Measured against + * the host, and it is the shape this specification exists to exclude --- an + * answer that is not true, given to a caller with no way to check it. + * + * ⭐ AND IT IS NOT THE `chmod' CASE, WHICH IS WHY IT IS HERE AND THAT IS NOT. + * A permission operation was declined because a FAT volume, a UEFI system + * partition and a Windows access-control list do not share a model. Every + * environment this specification targets locks a byte range and spells it + * almost identically. What was missing was a word, not a capability. + * + * ⚠️ Whether a VOLUME can is a property of the format rather than of the + * environment, exactly as it is for links --- a network volume may not, and a + * read-only medium need not. So it is an operation of this interface answered by + * `kal_fs_props' and not an interface of its own, which is what clause 6.2 + * requires and what makes it admissible: a caller asks first. + * + * `mode' is KAL_LOCK_SHARED or KAL_LOCK_EXCLUSIVE, optionally with KAL_LOCK_WAIT. + * Without KAL_LOCK_WAIT an operation that would block reports kal_err_again, + * which is the answer `fcntl' spells EAGAIN and the one a caller polls upon. + * + * A length of zero means "from `start' to the end, however far that comes to + * be", which is the whole-file convention every environment beneath spells the + * same way. + * + * ⭐⭐ A LOCK IS HELD BY THE FILE AND ENDS WITH IT, AND THAT IS REQUIRED RATHER + * THAN OBSERVED. It is released by `kal_fs_close_file' and by the end of the + * program that holds it, however that program ends. + * + * That requirement is the whole reason this belongs here rather than above the + * line: a caller CAN build exclusion for itself out of KAL_OPEN_EXCLUSIVE and a + * name, and nothing then releases that name when its holder dies --- so a + * program that ends abnormally while holding one locks itself out of its own + * file for ever. Release upon death is what only the environment can supply. + * + * ⚠️ AND IT IS THE FILE AND NOT THE PROGRAM, WHICH IS NARROWER THAN ONE + * ENVIRONMENT'S OLDEST FORM OF THIS. That form releases every lock a program + * holds upon a node as soon as the program closes ANY descriptor for it, so a + * library that opened the same file twice destroyed its own lock. An + * implementation shall not expose that: the holder is this `kal_file'. The + * environments that have the older form also have a newer one whose holder is + * exactly the open file, and that is the one to build upon. + * + * Two locks upon one file: an implementation may replace the earlier with the + * later where the ranges meet, which is what every environment beneath does. + * A caller that requires them distinct opens the file twice. */ +#define KAL_LOCK_SHARED ((kal_uintptr)1u << 0) +#define KAL_LOCK_EXCLUSIVE ((kal_uintptr)1u << 1) +#define KAL_LOCK_WAIT ((kal_uintptr)1u << 2) + +int kal_fs_lock(struct kal_file, kal_u64 start, kal_u64 len, kal_uintptr mode); +int kal_fs_unlock(struct kal_file, kal_u64 start, kal_u64 len); + +/* How much the volume a directory is on holds, and how much of that is free, + * in bytes. Version 0.10. + * + * An implementation that does not claim KAL_FS_PROP_CAPACITY for the volume + * reports kal_err_not_supported. One that claims it answers, and writes zero + * into a position it cannot distinguish --- which is the same convention + * `kal_task_parallelism' uses, and for the same reason: a caller must be able + * to tell "nothing available" from "not answered", and only one of the two is + * a number to act upon. + * + * Either pointer may be null, for a caller that wants one of the two. */ +int kal_fs_capacity(struct kal_dir, kal_u64* total, kal_u64* available); + /* Nodes whose content is another name. * * ⚠️ THESE ARE OPERATIONS OF THIS INTERFACE AND NOT AN INTERFACE OF THEIR OWN, diff --git a/include/openkal/process.h b/include/openkal/process.h index 3e396ae..ee39d43 100644 --- a/include/openkal/process.h +++ b/include/openkal/process.h @@ -72,6 +72,8 @@ struct kal_preopen { #define KAL_PROCESS_PROP_EXIT_STATUS ((kal_uintptr)1u << 2) #define KAL_PROCESS_PROP_CHANNEL ((kal_uintptr)1u << 3) #define KAL_PROCESS_PROP_GRANT_DIR ((kal_uintptr)1u << 4) +/* kal_process_spawn_bound is answered here. Version 0.10. */ +#define KAL_PROCESS_PROP_BOUND_LIFETIME ((kal_uintptr)1u << 5) #ifdef __cplusplus extern "C" { @@ -126,6 +128,52 @@ int kal_process_wait(struct kal_process, int* status, int* terminated); int kal_process_terminate(struct kal_process); void kal_process_close(struct kal_process); +/* Starts a program whose lifetime is BOUND to the caller's: when the calling + * image ends, however it ends, the started program ends too. Version 0.10. + * + * ⚠️⚠️ ADDED BECAUSE A LIBRARY ABOVE THIS INTERFACE HAD TO COMPOSE SOMETHING IT + * COULD NOT THEN CONTROL, AND A CALLER WAS TOLD A FALSEHOOD ABOUT IT. + * + * Clause 7.1 declines to replace a running image, correctly. A C library asked + * for `execve' therefore composes it: start the program, wait for it, end with + * its status. That composition leaves THREE images where a system with the + * operation has two --- the caller, the copy that is waiting, and the program. + * + * ⭐ AND THE MIDDLE ONE IS THE ONE A SIGNAL REACHES. `kal_process_terminate' + * upon the identifier the caller holds terminates the WAITER. Measured, with a + * host as control: identical status words, opposite outcomes --- the caller is + * told the program died on the signal it sent, while the program runs to + * completion, unsupervised. openkal-linux#13. + * + * ⚠️ `kal_process_terminate' is not at fault: it was asked to terminate one + * started program and did. What was missing was a way to SAY the thing `execve' + * means --- this program stands in for me, so it does not outlive me. + * + * Otherwise identical to kal_process_spawn. An implementation that cannot bind a + * lifetime does not claim KAL_PROCESS_PROP_BOUND_LIFETIME and reports + * kal_err_not_supported here rather than starting a program it cannot bind: a + * caller that asked for the binding asked for it, and a program started without + * it is not the program the caller asked to start. + * + * A SECOND DECLARATION RATHER THAN A FLAG ON THE FIRST, because clause 8 forbids + * altering an existing one --- the same reason `kal_process_spawn_with' is a + * declaration and not an argument. + * + * ⚠️ WHICH MEANS THE TWO ADDITIONS DO NOT COMBINE, AND THAT IS DELIBERATE. There + * is no form that both grants directories and binds a lifetime. Declaring every + * combination is how an interface acquires four spawns and then eight, so the + * combination is declared when something needs it and not before; clause 8 + * permits adding it then. What a caller must not do meanwhile is take + * `kal_process_spawn_with' and assume the binding: it does not bind, and a + * program that outlives its caller is exactly the failure this exists to + * remove. */ +int kal_process_spawn_bound(struct kal_dir base, + const char* path, kal_uintptr path_len, + const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, + const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, + const struct kal_spawn_streams* streams, + struct kal_process* out); + kal_uintptr kal_process_props(void); #ifdef __cplusplus diff --git a/include/openkal/task.h b/include/openkal/task.h index e40b184..dc7ca6c 100644 --- a/include/openkal/task.h +++ b/include/openkal/task.h @@ -64,6 +64,24 @@ void kal_task_yield(void); * same moment; may be reused after one finishes. */ kal_uintptr kal_task_current(void); +/* How many contexts this environment can run at the same moment, or zero where + * it cannot say. Version 0.10. + * + * ⚠️⚠️ ADDED BECAUSE ITS ABSENCE WAS A WRONG ANSWER RATHER THAN A REFUSAL. + * KAL_TASK_PROP_PARALLEL says WHETHER contexts run at the same moment and not + * HOW MANY can, and a C library above has no other place to look --- so + * `sysconf(_SC_NPROCESSORS_ONLN)' fell back to 1 and + * `std::thread::hardware_concurrency()' answered 1, with no error. A program + * sizing a pool of workers got one worker and no way to know. Measured: 1 here + * against 32 on the same machine's own C library. + * + * ⭐ ZERO IS "CANNOT SAY" AND IS NOT ONE. A caller must be able to tell an + * environment that has one processor from one that will not answer, because the + * two call for different behaviour: the first is a fact to size against, and + * the second is a reason to ask the operator. An implementation that does not + * claim KAL_TASK_PROP_PARALLEL answers 1, which is the truth there. */ +kal_uintptr kal_task_parallelism(void); + /* Suspends the calling context while the word at the given address holds the * given value, until another context wakes it or the timeout elapses. The * comparison and the suspension occur without an intervening opportunity for diff --git a/include/openkal/version.h b/include/openkal/version.h index 5cefd9f..7f5ddca 100644 --- a/include/openkal/version.h +++ b/include/openkal/version.h @@ -33,7 +33,7 @@ * older implementation reports conditions this consumer distinguishes as * conditions it does not, which is a wrong answer rather than a refusal. */ #define KAL_VERSION_MAJOR 0u -#define KAL_VERSION_MINOR 9u +#define KAL_VERSION_MINOR 10u #define KAL_VERSION_PATCH 0u #define KAL_VERSION_MAKE(major, minor, patch) \ diff --git a/kit/mcpp.toml b/kit/mcpp.toml index ace8d92..432ca9c 100644 --- a/kit/mcpp.toml +++ b/kit/mcpp.toml @@ -67,7 +67,7 @@ kind = "lib" # published packages, which is the only place this could be observed: no # continuous integration in this ecosystem resolves a published package, # because they all substitute working trees by design. -openkal = "0.9.0" +openkal = "0.10.0" # ⚠️ NO `include_dirs` HERE EITHER. It named `../include`, which reaches out of # this package into whatever happens to sit above it. With the specification @@ -98,4 +98,4 @@ openkal = "0.9.0" # same reason: a change spanning the specification and an implementation must be # tested against both halves as written, not against whichever half is published. [target.'cfg(os = "linux")'.dev-dependencies] -openkal-linux = { version = "0.7.1", features = ["standalone"] } +openkal-linux = { version = "0.9.0", features = ["standalone"] } diff --git a/mcpp.toml b/mcpp.toml index f9ff3c0..1ea4902 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.9.0" +version = "0.10.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/fs.cppm b/src/fs.cppm index c7422c7..ec0e914 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -46,6 +46,10 @@ export using ::kal_fs_truncate; export using ::kal_fs_info; export using ::kal_fs_file_info; export using ::kal_fs_set_modified; +export using ::kal_fs_set_modified_at; +export using ::kal_fs_lock; +export using ::kal_fs_unlock; +export using ::kal_fs_capacity; export using ::kal_fs_mkdir; export using ::kal_fs_remove; export using ::kal_fs_rename; @@ -99,6 +103,8 @@ inline constexpr props links {KAL_FS_PROP_LINKS}; inline constexpr props modified_time {KAL_FS_PROP_MODIFIED_TIME}; inline constexpr props atomic_rename {KAL_FS_PROP_ATOMIC_RENAME}; inline constexpr props make_links {KAL_FS_PROP_MAKE_LINKS}; +inline constexpr props locks {KAL_FS_PROP_LOCKS}; +inline constexpr props capacity {KAL_FS_PROP_CAPACITY}; enum : int { seek_set = KAL_SEEK_SET, seek_current = KAL_SEEK_CURRENT, seek_end = KAL_SEEK_END }; @@ -117,6 +123,30 @@ inline constexpr open_flags truncate {KAL_OPEN_TRUNCATE}; inline constexpr open_flags append {KAL_OPEN_APPEND}; } +// The mode of kal_fs_lock, in its own kind for the reason `open_flags' is in +// one: an intent and a capability word are different things, and a word that +// serves as both can be passed to the wrong operation. +// +// ⚠️⚠️ AND IT IS HERE BECAUSE A `#define' DOES NOT CROSS A MODULE BOUNDARY. +// The header's macros are invisible to a consumer that writes `import +// openkal.fs', so a position added to the C header and not to this file is a +// position half the consumers cannot name. The specification's own conformance +// suite is such a consumer, and it is what reported the omission: +// +// error: use of undeclared identifier 'KAL_LOCK_EXCLUSIVE' +struct lock_tag; +using lock_mode = kal::props; + +namespace lock { +inline constexpr lock_mode shared {KAL_LOCK_SHARED}; +inline constexpr lock_mode exclusive{KAL_LOCK_EXCLUSIVE}; +inline constexpr lock_mode wait {KAL_LOCK_WAIT}; +} + +inline int lock_range(file f, kal_u64 start, kal_u64 len, lock_mode mode) { + return kal_fs_lock(f, start, len, mode.bits); +} + inline int open_file(dir base, const char* name, kal_uintptr len, open_flags flags, file* out) { return kal_fs_open(base, name, len, flags.bits, out); diff --git a/src/process.cppm b/src/process.cppm index bb76cf6..2ed3666 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -26,6 +26,7 @@ export using ::kal_preopen; export using ::kal_process_spawn; export using ::kal_process_spawn_with; +export using ::kal_process_spawn_bound; export using ::kal_process_channel; export using ::kal_process_channel_close; export using ::kal_process_wait; @@ -50,6 +51,7 @@ inline constexpr props stream_passing{KAL_PROCESS_PROP_STREAM_PASSING}; inline constexpr props exit_status {KAL_PROCESS_PROP_EXIT_STATUS}; inline constexpr props channel {KAL_PROCESS_PROP_CHANNEL}; inline constexpr props grant_dir {KAL_PROCESS_PROP_GRANT_DIR}; +inline constexpr props bound_lifetime{KAL_PROCESS_PROP_BOUND_LIFETIME}; inline props properties() { return props{kal_process_props()}; } inline bool has(props p) { return properties().has(p); } diff --git a/src/task.cppm b/src/task.cppm index a9ae903..82d6d0d 100644 --- a/src/task.cppm +++ b/src/task.cppm @@ -23,6 +23,7 @@ export using ::kal_task_start; export using ::kal_task_join; export using ::kal_task_yield; export using ::kal_task_current; +export using ::kal_task_parallelism; export using ::kal_task_wait; export using ::kal_task_wake; export using ::kal_task_props;