diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d368f0..034c79b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ env: # A version verified to build these packages, not a measured minimum. The pin # exists for reproducibility rather than because an older mcpp is known to # fail. - MCPP_VERSION: 2026.8.26.2 + MCPP_VERSION: 2026.8.27.1 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' @@ -717,6 +717,88 @@ jobs: # unconditionally would fail to link against a conforming implementation # of three. Selecting three is therefore asserted to produce a report # rather than a link failure. + # openkal-kit, which is in this repository and is not this specification. + # + # The kit composes facilities from openkal's interfaces --- awaiting a + # channel, writing an endpoint down and reading one back --- that the + # specification declines to admit because they can be composed. Running its + # tests here is what keeps the composition working as the interfaces it + # rests upon change. + # + # THE SURFACE CHECK ABOVE IS WHAT KEEPS THE TWO APART, and it does so by + # the shape of what is exported rather than by a declaration. The kit is + # C++ modules in `namespace kal::kit` and exports no name beginning with + # `kal_`; a C++ name is mangled to `_ZN3kal3kit…`, so an implementation + # that linked the kit still passes `check-surface.sh --complete`. + - name: The kit, composed from the interfaces above + if: matrix.implementation == 'openkal-linux' + run: | + bash tools/run-kit-tests.sh '${{ matrix.implementation }}' .impl + + # ⭐⭐ THE EXAMPLE A CONSUMER READS, BUILT AND RUN. + # + # examples/portable is the program the specification points at as the one + # every implementation must satisfy, and NOTHING BUILT IT. Its README + # stated that "an implementation's continuous integration fetches this + # source ... builds it against itself, and asserts the lines below"; + # measured across all six implementation repositories, the word `portable` + # appeared in none of their workflows. A promise with nothing executing it. + # + # ⚠️ AND THE DRIFT THAT FOLLOWS FROM THAT IS VISIBLE IN ITS OWN MANIFEST: + # it named `openkal = "0.5.0"` --- three specification versions behind --- + # while claiming to demonstrate the current contract to a reader who would + # copy it. Nothing could have noticed. + # + # The manifest names published versions, because that is what a consumer + # writes. Those do not exist while a change spanning these repositories is + # under review, so the working trees stand in, exactly as + # tools/run-conformance.sh does for the suite. + # + # ⚠️ RELATIVE PATHS, NOT `pwd`. This job runs on Windows too, where MSYS + # reports `/d/a/...`, which the build tool does not resolve. Relative + # paths also cannot put a local directory into a file that might be + # committed. + # + # ⚠️ NO `standalone` FEATURE HERE. That feature makes the implementation + # supply `_start`, which collides with the C library's crt1.o on a hosted + # toolchain --- measured: "multiple definition of `_start`". The + # conformance runner defaults to no features for the same reason. + - name: The portable program builds and runs over this implementation + working-directory: examples/portable + run: | + set -euo pipefail + subst() { sed -E "$2" "$1" > "$1.next" && mv "$1.next" "$1"; } + subst mcpp.toml 's|^openkal = .*$|openkal = { path = "../.." }|' + subst mcpp.toml 's|^${{ matrix.implementation }} = .*$|${{ matrix.implementation }} = { path = "../../.impl" }|' + grep -q 'path = "../../.impl"' mcpp.toml \ + || { echo "::error::the implementation was not substituted"; exit 1; } + + # ⚠️ THE IMPLEMENTATION REACHES THE SPECIFICATION TOO, AND BY ITS OWN + # FORM. Substituting only this manifest leaves two routes to one + # package and the build refuses, correctly: + # + # error: dependency 'mcpplibs.openkal' is requested as both a path + # dep (by 'portable') and a git dep (by 'openkal-linux@path') + # + # Both must name the same directory, which is what run-conformance.sh + # does for the suite. From `.impl/` the specification is `..`; the two + # spellings resolve to one canonical path and therefore to one node. + subst ../../.impl/mcpp.toml 's|^openkal = .*$|openkal = { path = ".." }|' + grep -q 'openkal = { path = ".." }' ../../.impl/mcpp.toml \ + || { echo "::error::the implementation still reaches openkal its own way"; exit 1; } + + mcpp run 2>&1 | tee portable.log + + # ⭐ THE COUNT IS THE CRITERION, AND SO IS THE DENOMINATOR. A program + # that failed to start would print none of these lines, and a check + # for the absence of failures would hold over an empty log. + lines=$(grep -c '^openkal: ' portable.log) + [ "$lines" -ge 10 ] \ + || { echo "::error::the program printed $lines lines; it reports one per interface"; exit 1; } + grep -q 'openkal: observations that did not hold: 0' portable.log \ + || { echo "::error::the portable program reported observations that did not hold"; exit 1; } + echo " the portable program held every observation over ${{ matrix.implementation }}" + - name: A selection of three interfaces is examined, not refused run: | rm -rf conformance/target diff --git a/README.md b/README.md index d7a1dcd..e80dd48 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,40 @@ under the compiler its author used is a description of that compiler. ## License Apache-2.0. + +## openkal-kit + +`kit/` holds facilities composed from the interfaces this specification defines. +It is a separate package, `mcpplibs/openkal-kit`, and it is **not** part of the +specification. + +The specification admits an interface only when it is a minimal capability every +kernel has and cannot be composed from the interfaces already present. That rule +is what keeps openkal implementable on a machine with firmware and nothing else, +and it leaves a gap: a program that wants to carry bytes between two of its own +contexts, or to turn `"127.0.0.1:8080"` into an endpoint, has an answer in POSIX +and no answer here — because both are composed rather than primitive. + +That gap was being filled by the port layer. There is one port layer today and +what it composes is POSIX, so a native openkal program either wrote the +composition again or took a whole C library. The kit is where the composition is +written once. + +**The contract form is what makes the two unmistakable.** Clause 10 states that +openkal's contract is a C application binary interface. The kit deliberately is +not one: it is C++ modules in `namespace kal::kit`, and it exports no name +beginning with `kal_`. Measured on its objects: the defined names are C++ mangled +module initialisers such as `_ZGIW7openkalW3kitW7channel`, and the operations are +inline and emitted into consumers rather than exported at all. So +`tools/check-surface.sh --complete` does not read a program that links the kit as +an implementation which has added names — the rule that checker enforces is about +the C surface, and the kit has none. + +So "is this normative" is answered by the shape of what is exported rather than +by a sentence saying it is not. A sentence can be overlooked; a mangled name +cannot become a C symbol. + +The consequence is the one that matters. Clause 8 forbids the specification from +altering a declaration it has published, which is what makes openkal safe to +depend upon and what makes it the wrong place for a facility still finding its +shape. **The kit may evolve.** diff --git a/SPEC.md b/SPEC.md index cae855b..30ea755 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.7 +# openkal Specification, version 0.8 ## 1. Scope @@ -43,11 +43,24 @@ provides an interface in whole or not at all. | `openkal.process` | a program image that has been started | standard | | `openkal.task` | an execution context, and a suspension primitive | standard | | `openkal.exec` | a region of the address space a program may execute | optional | +| `openkal.terminal` | an interactive stream's treatment of what is typed | optional | +| `openkal.net` | a connection, and a listener for connections | optional | +| `openkal.datagram` | a message with a boundary, sent without a connection | optional | +| `openkal.space` | an address space, and a context executing in one | optional | +| `openkal.timeout` | a bound upon operations that would otherwise wait | optional | | `openkal.event` | readiness of a set of resources | reserved | -Version 0.7 specifies the core, standard and optional interfaces. The reserved +Version 0.8 specifies the core, standard and optional interfaces. The reserved row is not specified, and its name shall not be used for other purposes. +The five interfaces added in version 0.8 are optional in the sense clause 3 +defines, and their optionality is not a concession. An environment with no +network is not deficient for providing neither `openkal.net` nor +`openkal.datagram`; an environment with no memory management unit cannot +provide `openkal.space` and is not deficient either. Clause 6.1 expresses each +absence as the absence of a definition at the link, so a program that requires +one of them is refused when it is built rather than when it runs. + *Core* denotes an interface every implementation provides. *Standard* denotes one an implementation hosting a C library provides. *Optional* denotes one it may omit without ceasing to host a C library, at the cost of the facilities @@ -339,7 +352,9 @@ This clause adds no declaration and alters none, so clause 8's rule is not engaged and the version does not advance on its account. Every declaration of version 0.6 already satisfies it — one hundred and one of them, examined by the procedure below — and what is new is that the property is now stated and -checked rather than held by the care of whoever wrote each header. +checked rather than held by the care of whoever wrote each header. The five +interfaces added in version 0.8 were examined by the same procedure and satisfy +it also; the count is now one hundred and forty-six. Clause 9's procedure examines this. @@ -413,6 +428,44 @@ consumer imports, which contradicts clause 4. implementation declares them. Not adopted: a record can disagree with the code it describes, and the file is a second place in which a package is configured. +Two further arrangements were weighed while specifying version 0.8 and are +recorded on the same basis. + +**Readiness notification.** An interface reporting that a stream may be read, by +waking a word as `kal_task_wake` does, was considered as the remedy for a context +that would otherwise wait without end. It composes better than the bound this +specification adopted: one operation covers every waitable resource, a single +context may await many sources, and a library above it needs no read-ahead buffer +because a notification consumes nothing. + +It was not adopted because of what it asks of an implementation. On an +environment whose readiness is discovered by polling a set of descriptors, an +implementation would have to maintain that set and a context of its own to watch +it. That is a mechanism reconstructed rather than a facility conveyed, which +clause 7.1 excludes. `openkal.timeout` asks the same environment only for what it +already does at the point of the call. + +**A space as a handle.** An earlier form of `openkal.space` separated the copying +of an address space from the starting of a context in it, so that a caller held a +space and could start a context in it afterwards. It was withdrawn while the +first implementation was being written. + +No environment this specification targets has that pair as a primitive. The copy +and the start are one act, and an implementation asked to separate them would +have to start a context anyway, park it upon a waiting primitive, and build a +channel by which to tell it what to run. Clause 7.1 identifies that as a fault in +the shape of the specification rather than in the implementation, and the +separated form was the shape at fault. The single operation that replaced it is +what every such environment already performs. + +**An instant rather than a duration.** `openkal.timeout` states a duration +because `kal_task_wait` does. An instant would not accumulate drift when a caller +retries in a loop, and was considered for that reason. It was not adopted because +it would give one specification two spellings of one idea. A caller that requires +an instant computes the remaining duration from `kal_time_monotonic`, so the cost +falls upon the caller that has the requirement rather than upon every +implementation. + The measurements that constrain any future proposal: 1. A requires-expression naming a qualified entity that does not exist is @@ -787,3 +840,33 @@ The following are recorded so that they are not mistaken for oversights. in its symbol names. Clause 8 protects the interface by prohibiting change rather than by permitting coexistence, and an ecosystem that outgrows that prohibition will require a mechanism this version does not define. +4. **Readiness.** Awaiting one of several sources is not an operation of this + specification. It is reached above the interface, from `openkal.task` and a + bound upon each wait; clause 6.3 records the alternative that was weighed and + the property of implementations that excluded it. +5. **Name resolution.** `openkal.net` and `openkal.datagram` carry an address and + a port. Turning a name into one is excluded by clause 3.4 and remains so: an + implementation shall not be required to parse an unbounded set of name + schemes. +6. **Permission and ownership of files.** Not defined, and not a deferral. A + permission presupposes an identity, and the environments this specification + targets do not agree that one exists. A C library above openkal reports the + absence as the error its own surface defines. +7. **Creation and reading of links.** Not defined, and not a deferral, for the + reason clause 6.4 gives: whether a filesystem has links is a property of the + format rather than of the environment. `KAL_FS_PROP_LINKS` reports it, and + resolution follows one where the property is claimed. +8. **Duplication of the calling image.** `fork` is refused by clause 7.1 and that + refusal stands. It is refused as an OPERATION. The atomic capabilities from + which a library may compose it are specified: `openkal.space` clones an + address space and starts a context in one, and `KAL_SPACE_PROP_CLONE_HANDLES` + states whether the handles accompany the memory. What this specification + declines to do is duplicate execution state, which a library above the + interface performs with the compiler's own facilities. A sentence reading + "openkal will not have fork" would have buried that distinction, and this + entry exists so that it is not written. +9. **Transfer of a handle between address spaces.** `kal_space_start` conveys no + handle, and `kal_process_channel` conveys a stream only across a spawn. A + 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. diff --git a/SURFACE.txt b/SURFACE.txt index c33abe7..25731fc 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -1,4 +1,4 @@ -# The C surface of openkal 0.6, one name per line. +# The C surface of openkal 0.8, 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 @@ -57,9 +57,12 @@ kal_fs_set_modified kal_fs_stream kal_fs_truncate # openkal.process +kal_process_channel +kal_process_channel_close kal_process_close kal_process_props kal_process_spawn +kal_process_spawn_with kal_process_terminate kal_process_wait # openkal.task @@ -75,3 +78,37 @@ kal_exec_alloc kal_exec_free kal_exec_props kal_exec_publish +# openkal.terminal +kal_terminal_get_mode +kal_terminal_props +kal_terminal_set_mode +kal_terminal_size +# openkal.net +kal_net_accept +kal_net_close +kal_net_close_listener +kal_net_connect +kal_net_listen +kal_net_listener_local +kal_net_local +kal_net_peer +kal_net_props +kal_net_shutdown +kal_net_stream +# openkal.datagram +kal_datagram_close +kal_datagram_local +kal_datagram_open +kal_datagram_props +kal_datagram_recv_from +kal_datagram_send_to +# openkal.space +kal_space_props +kal_space_start +# openkal.timeout +kal_timeout_accept +kal_timeout_granularity_ns +kal_timeout_read +kal_timeout_recv_from +kal_timeout_wait_process +kal_timeout_write diff --git a/conformance/mcpp.toml b/conformance/mcpp.toml index 0526dc5..1caa4bc 100644 --- a/conformance/mcpp.toml +++ b/conformance/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-conformance" -version = "0.5.1" +version = "0.6.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.7.0" +openkal = "0.8.0" # The implementation under examination is not named here. # @@ -62,6 +62,16 @@ process = ["fs"] # a program is started relative to a directory task = [] exec = [] +# The five interfaces version 0.8 adds. Each is optional in the sense clause 3 +# defines, so each is a feature and none is in `hosted': a set demanding an +# optional interface would turn a permitted choice into a link error, which is +# the mistake the note above `optional' records having been made once already. +terminal = [] +net = [] +datagram = [] +space = ["process"] # a started context is waited for as a process +timeout = [] + # The specification names this set `hosted' (clause 3.3). Both spellings are # here because the workflows and the older invocations use `standard', and a # rename that broke them would be a rename of the wrong thing. @@ -91,7 +101,7 @@ standard = ["hosted"] # bare-metal one provides it only if its board has a source, and neither is a # deviation. The caller names this set for an implementation that provides # these interfaces and omits it for one that does not. -optional = ["exec", "random"] +optional = ["exec", "random", "terminal", "net", "datagram", "space", "timeout"] # The kinds of examination. Behaviour is always performed; the other three are # selected, because each costs time that a reader running the suite to answer diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index 10a3a4b..7f62254 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -29,6 +29,12 @@ void okc_declarations_c(void) { (void)sizeof(&kal_abort); (void)sizeof(&kal_alloc); + (void)sizeof(&kal_datagram_close); + (void)sizeof(&kal_datagram_local); + (void)sizeof(&kal_datagram_open); + (void)sizeof(&kal_datagram_props); + (void)sizeof(&kal_datagram_recv_from); + (void)sizeof(&kal_datagram_send_to); (void)sizeof(&kal_env_arg); (void)sizeof(&kal_env_arg_count); (void)sizeof(&kal_env_var); @@ -59,13 +65,29 @@ void okc_declarations_c(void) (void)sizeof(&kal_fs_set_modified); (void)sizeof(&kal_fs_stream); (void)sizeof(&kal_fs_truncate); + (void)sizeof(&kal_net_accept); + (void)sizeof(&kal_net_close); + (void)sizeof(&kal_net_close_listener); + (void)sizeof(&kal_net_connect); + (void)sizeof(&kal_net_listen); + (void)sizeof(&kal_net_listener_local); + (void)sizeof(&kal_net_local); + (void)sizeof(&kal_net_peer); + (void)sizeof(&kal_net_props); + (void)sizeof(&kal_net_shutdown); + (void)sizeof(&kal_net_stream); + (void)sizeof(&kal_process_channel); + (void)sizeof(&kal_process_channel_close); (void)sizeof(&kal_process_close); (void)sizeof(&kal_process_props); (void)sizeof(&kal_process_spawn); + (void)sizeof(&kal_process_spawn_with); (void)sizeof(&kal_process_terminate); (void)sizeof(&kal_process_wait); (void)sizeof(&kal_random_fill); (void)sizeof(&kal_random_props); + (void)sizeof(&kal_space_props); + (void)sizeof(&kal_space_start); (void)sizeof(&kal_stderr); (void)sizeof(&kal_stdin); (void)sizeof(&kal_stdout); @@ -80,8 +102,18 @@ void okc_declarations_c(void) (void)sizeof(&kal_task_wait); (void)sizeof(&kal_task_wake); (void)sizeof(&kal_task_yield); + (void)sizeof(&kal_terminal_get_mode); + (void)sizeof(&kal_terminal_props); + (void)sizeof(&kal_terminal_set_mode); + (void)sizeof(&kal_terminal_size); (void)sizeof(&kal_time_monotonic); (void)sizeof(&kal_time_monotonic_granularity); + (void)sizeof(&kal_timeout_accept); + (void)sizeof(&kal_timeout_granularity_ns); + (void)sizeof(&kal_timeout_read); + (void)sizeof(&kal_timeout_recv_from); + (void)sizeof(&kal_timeout_wait_process); + (void)sizeof(&kal_timeout_write); (void)sizeof(&kal_time_props); (void)sizeof(&kal_time_sleep); (void)sizeof(&kal_time_wall); diff --git a/conformance/src/sections/datagram.cpp b/conformance/src/sections/datagram.cpp new file mode 100644 index 0000000..dca0cc1 --- /dev/null +++ b/conformance/src/sections/datagram.cpp @@ -0,0 +1,114 @@ +module okc.datagram; + +import openkal.types; +import openkal.datagram; +import okc.report; +import okc.spec; + +namespace okc::datagram { + +#ifdef MCPP_FEATURE_DATAGRAM +namespace { + +kal_endpoint loopback_v4(kal_u32 port) { + kal_endpoint ep{}; + ep.addr[0] = 127; ep.addr[1] = 0; ep.addr[2] = 0; ep.addr[3] = 1; + ep.addr_len = 4; + ep.port = port; + return ep; +} + +} // namespace +#endif + +void run() { + heading("openkal.datagram"); +#ifndef MCPP_FEATURE_DATAGRAM + unobserved(kind::behaviour, "openkal.datagram", "the interface was not selected"); + return; +#else + claim("kal_datagram_props", kal_datagram_props); + + kal_endpoint want = loopback_v4(0); + kal_datagram receiver{}; + const int orc = kal_datagram_open(&want, &receiver); + if (orc != kal_ok) { + unobserved(kind::behaviour, "openkal.datagram", + "a loopback endpoint could not be opened in this environment"); + return; + } + observe(kind::behaviour, true, "an endpoint opens on the loopback address"); + + kal_endpoint bound{}; + const int brc = kal_datagram_local(receiver, &bound); + observe(kind::behaviour, brc == kal_ok && bound.port != 0, + "an endpoint opened on port zero reports the port it was given"); + if (brc != kal_ok || bound.port == 0) { kal_datagram_close(receiver); return; } + + kal_datagram sender{}; + const int src = kal_datagram_open(nullptr, &sender); + observe(kind::behaviour, src == kal_ok, + "an endpoint that only sends is opened without a local address"); + if (src != kal_ok) { kal_datagram_close(receiver); return; } + + kal_endpoint to = loopback_v4(bound.port); + + // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is the property that + // distinguishes this interface from a stream. The count reported is + // therefore always the length that was given. + { + const char msg[] = "openkal"; + const kal_io_result w = kal_datagram_send_to(sender, msg, sizeof msg - 1, &to); + observe(kind::behaviour, w.e == kal_ok && w.n == sizeof msg - 1, + "a message is sent whole and the count is the length given"); + + kal_endpoint from{}; + char buf[16] = {0}; + const kal_io_result r = kal_datagram_recv_from(receiver, buf, sizeof buf, &from); + bool same = r.e == kal_ok && r.n == sizeof msg - 1; + for (kal_uintptr i = 0; same && i < r.n; ++i) + if (buf[i] != msg[i]) same = false; + observe(kind::behaviour, same, "the message received is the message sent"); + observe(kind::behaviour, r.e != kal_ok || from.addr_len == 4, + "the sender of a received message is reported"); + } + + // A MESSAGE LONGER THAN THE BUFFER IS TRUNCATED AND THE EXCESS IS LOST, + // which is what the medium does. What is observed is that the operation + // reports what it placed in the buffer rather than what was sent, because a + // caller that trusted the larger number would read beyond its own buffer. + { + char big[64]; + for (auto& c : big) c = 'x'; + const kal_io_result w = kal_datagram_send_to(sender, big, sizeof big, &to); + if (w.e == kal_ok) { + char small[8] = {0}; + kal_endpoint from{}; + const kal_io_result r = kal_datagram_recv_from(receiver, small, sizeof small, &from); + observe(kind::behaviour, r.e == kal_ok && r.n <= sizeof small, + "a truncated message reports the count placed in the buffer"); + } else { + unobserved(kind::behaviour, "truncation reports the buffered count", + "the larger message could not be sent"); + } + } + + kal_datagram_close(sender); + kal_datagram_close(receiver); + + // An endpoint of a length this implementation does not know is refused + // rather than read as one it does, for the same reason as in openkal.net. + { + kal_endpoint odd{}; + odd.addr_len = 7; + odd.port = 9; + kal_datagram d{}; + const int rc = kal_datagram_open(&odd, &d); + observe(kind::behaviour, rc == kal_err_invalid, + "an endpoint of unknown length is refused, not misread"); + if (rc == kal_ok) kal_datagram_close(d); + } +#endif +} + +} diff --git a/conformance/src/sections/datagram.cppm b/conformance/src/sections/datagram.cppm new file mode 100644 index 0000000..93a4c99 --- /dev/null +++ b/conformance/src/sections/datagram.cppm @@ -0,0 +1,6 @@ +// The section that examines openkal.datagram. +export module okc.datagram; + +export namespace okc::datagram { +void run(); +} diff --git a/conformance/src/sections/net.cpp b/conformance/src/sections/net.cpp new file mode 100644 index 0000000..05e5d2d --- /dev/null +++ b/conformance/src/sections/net.cpp @@ -0,0 +1,148 @@ +module okc.net; + +import openkal.types; +import openkal.stream; +import openkal.net; +import okc.report; +import okc.spec; + +namespace okc::net { + +#ifdef MCPP_FEATURE_NET +namespace { + +// The loopback address, written byte by byte because that is what an endpoint +// carries. Constructing it here rather than parsing a string is the whole point +// of clause 3.4: this suite resolves no name. +kal_endpoint loopback_v4(kal_u32 port) { + kal_endpoint ep{}; + ep.addr[0] = 127; ep.addr[1] = 0; ep.addr[2] = 0; ep.addr[3] = 1; + ep.addr_len = 4; + ep.port = port; + return ep; +} + +} // namespace +#endif + +void run() { + heading("openkal.net"); +#ifndef MCPP_FEATURE_NET + unobserved(kind::behaviour, "openkal.net", "the interface was not selected"); + return; +#else + claim("kal_net_props", kal_net_props); + + // A LISTENER ON PORT ZERO, AND THE PORT READ BACK. The environment chooses + // the port, so a suite that named one would fail on a machine where that + // port was in use --- and would be examining the machine rather than the + // implementation. Reading it back is also the observation that + // kal_net_listener_local answers, which is the inverse clause 7.11 requires. + kal_endpoint want = loopback_v4(0); + kal_net_listener listener{}; + const int lrc = kal_net_listen(&want, &listener); + if (lrc != kal_ok) { + unobserved(kind::behaviour, "openkal.net", + "a loopback listener could not be opened in this environment"); + return; + } + observe(kind::behaviour, true, "a listener opens on the loopback address"); + + kal_endpoint bound{}; + const int brc = kal_net_listener_local(listener, &bound); + observe(kind::behaviour, brc == kal_ok && bound.port != 0, + "a listener opened on port zero reports the port it was given"); + observe(kind::behaviour, brc == kal_ok && bound.addr_len == 4, + "the reported endpoint carries the length of the address it holds"); + + if (brc != kal_ok || bound.port == 0) { + kal_net_close_listener(listener); + return; + } + + // A connection to that listener, accepted, and bytes carried across it. The + // transfer uses kal_stream_read and kal_stream_write and not an operation of + // this interface, which is the property that makes a connection a stream. + kal_endpoint to = loopback_v4(bound.port); + kal_net_conn client{}; + const int crc = kal_net_connect(&to, &client); + observe(kind::behaviour, crc == kal_ok, "a connection to the listener is established"); + if (crc != kal_ok) { kal_net_close_listener(listener); return; } + + kal_net_conn server{}; + const int arc = kal_net_accept(listener, &server); + observe(kind::behaviour, arc == kal_ok, "the listener accepts the connection"); + if (arc != kal_ok) { kal_net_close(client); kal_net_close_listener(listener); return; } + + // The streams the connections own. Borrowed and released with the + // connection, which is the arrangement openkal.fs already uses. + const kal_stream cs{kal_net_stream(client)}; + const kal_stream ss{kal_net_stream(server)}; + + { + const char msg[] = "openkal"; + const kal_io_result w = kal_stream_write(cs, msg, sizeof msg - 1); + observe(kind::behaviour, w.e == kal_ok && w.n == sizeof msg - 1, + "a connection carries bytes through the stream operations"); + + char buf[16] = {0}; + const kal_io_result r = kal_stream_read(ss, buf, sizeof buf); + bool same = r.e == kal_ok && r.n == sizeof msg - 1; + for (kal_uintptr i = 0; same && i < r.n; ++i) + if (buf[i] != msg[i]) same = false; + observe(kind::behaviour, same, "the bytes read are the bytes written"); + } + + // The peer of each end is the other end. Observed on the accepted side, + // whose peer is the client's local address; the two are the same machine + // here, so what is asserted is that the lengths agree and the call answers. + { + kal_endpoint peer{}; + const int rc = kal_net_peer(server, &peer); + observe(kind::behaviour, rc == kal_ok && peer.addr_len == 4, + "an accepted connection reports its peer"); + } + + // HALF-CLOSURE IS WHAT DISTINGUISHES A CONNECTION FROM A FILE, and it is + // observed only where the implementation claims it. An implementation that + // cannot express it refuses and withholds the position, and both of those + // are conforming; what would not be conforming is claiming the position and + // then refusing. + { + const bool claims = kal::net::has(kal::net::halfclose); + const int rc = kal::net::shutdown(client, kal::net::shut::write); + if (claims) { + observe(kind::behaviour, rc == kal_ok, + "a claimed half-closure is performed when asked for"); + char buf[4] = {0}; + const kal_io_result r = kal_stream_read(ss, buf, sizeof buf); + observe(kind::behaviour, r.e == kal_ok && r.n == 0, + "the peer observes end of input after a half-closure"); + } else { + observe(kind::behaviour, rc == kal_err_not_supported, + "an unclaimed half-closure is refused rather than ignored"); + } + } + + kal_net_close(server); + kal_net_close(client); + kal_net_close_listener(listener); + + // An endpoint whose length this implementation does not know is refused + // rather than read as one it does. This is the evolution rule of the + // endpoint type, and an implementation that ignored the length would + // misread every address a later revision defines. + { + kal_endpoint odd{}; + odd.addr_len = 7; // not 4, not 16, not 20 + odd.port = 9; + kal_net_conn c{}; + const int rc = kal_net_connect(&odd, &c); + observe(kind::behaviour, rc == kal_err_invalid, + "an endpoint of unknown length is refused, not misread"); + if (rc == kal_ok) kal_net_close(c); + } +#endif +} + +} diff --git a/conformance/src/sections/net.cppm b/conformance/src/sections/net.cppm new file mode 100644 index 0000000..a5a656c --- /dev/null +++ b/conformance/src/sections/net.cppm @@ -0,0 +1,6 @@ +// The section that examines openkal.net. +export module okc.net; + +export namespace okc::net { +void run(); +} diff --git a/conformance/src/sections/process.cpp b/conformance/src/sections/process.cpp index 7f55965..807daaf 100644 --- a/conformance/src/sections/process.cpp +++ b/conformance/src/sections/process.cpp @@ -2,6 +2,7 @@ module okc.process; import openkal.types; import openkal.process; +import openkal.stream; // the channel is read and written through it import openkal.fs; import openkal.env; import openkal.time; @@ -89,13 +90,61 @@ void run() { "the implementation does not claim prop_terminate"); } + // THE THREE OPERATIONS VERSION 0.8 ADDS TO THIS INTERFACE. + // + // They are examined here and not in a section of their own, because they are + // part of openkal.process: clause 6.1 makes an interface provided IN PART a + // deviation, so an implementation reaching this section has undertaken to + // provide them. + { + kal_stream mine{}, theirs{}; + const int rc = kal_process_channel(&mine, &theirs); + observe(kind::behaviour, rc == kal_ok, "a channel is created"); + if (rc == kal_ok) { + const char msg[] = "through the channel"; + const kal_io_result w = kal_stream_write(theirs, msg, sizeof msg - 1); + observe(kind::behaviour, w.e == kal_ok && w.n == sizeof msg - 1, + "the far end of a channel accepts bytes"); + + char buf[64] = {0}; + const kal_io_result r = kal_stream_read(mine, buf, sizeof buf); + bool same = r.e == kal_ok && r.n == sizeof msg - 1; + for (kal_uintptr i = 0; same && i < r.n; ++i) + if (buf[i] != msg[i]) same = false; + observe(kind::behaviour, same, "the near end reads what the far end wrote"); + + // THE END OF INPUT IS WHAT THE RELEASE IS FOR. A parent that does not + // release the far end after a spawn never observes it, which is the + // deadlock this pair invites and the reason the release is declared + // beside the operation rather than left to openkal.stream. + kal_process_channel_close(theirs); + const kal_io_result eof = kal_stream_read(mine, buf, sizeof buf); + observe(kind::behaviour, eof.e == kal_ok && eof.n == 0, + "closing the far end is observed as end of input on the near one"); + kal_process_channel_close(mine); + } + + // The property word and the operations must agree, which is what clause + // 6.2 requires of a word and is the only thing that can be observed of + // an operation an implementation declines. + if (kal::process::has(kal::process::channel)) + observe(kind::behaviour, rc == kal_ok, + "a claimed channel is provided when asked for"); + else + observe(kind::behaviour, rc == kal_err_not_supported, + "an unclaimed channel is refused rather than half provided"); + } + if (performs(kind::abi)) { observe(kind::abi, sizeof(kal_process) == sizeof(kal_uintptr), "a process handle occupies one machine word"); observe(kind::abi, sizeof(kal_spawn_streams) == 3 * sizeof(kal_uintptr), "the stream selection occupies three machine words"); const kal_uintptr assigned = (kal::process::terminate | kal::process::stream_passing - | kal::process::exit_status).bits; + | kal::process::exit_status | kal::process::channel + | kal::process::grant_dir).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"); } diff --git a/conformance/src/sections/space.cpp b/conformance/src/sections/space.cpp new file mode 100644 index 0000000..2aab173 --- /dev/null +++ b/conformance/src/sections/space.cpp @@ -0,0 +1,117 @@ +module okc.space; + +import openkal.types; +import openkal.abort; +import openkal.process; +import openkal.space; +import okc.report; +import okc.spec; + +namespace okc::space { + +#ifdef MCPP_FEATURE_SPACE +namespace { + +// What the started context does. It is deliberately the smallest thing a +// context can do: the observation is that a context ran in a copy of the +// caller's space, not that it computed anything. A larger entry would make a +// failure ambiguous between "the copy did not work" and "the entry was wrong". +// +// THE EXIT STATUS IS THE CHANNEL. The started context has an address space of +// its own, so a store it performs is not visible to the caller; the one thing it +// can report is how it ended, and kal_process_wait is what reads that. +void entry(void* arg) { + const int code = (arg == nullptr) ? 1 : 7; + kal_exit(code); +} + +// A stack for the started context. Its top is passed, because that is the end +// execution begins from on every architecture this specification targets. +alignas(16) unsigned char child_stack[64 * 1024]; + +} // namespace +#endif + +void run() { + heading("openkal.space"); +#ifndef MCPP_FEATURE_SPACE + unobserved(kind::behaviour, "openkal.space", "the interface was not selected"); + return; +#else + claim("kal_space_props", kal_space_props); + + // THE PROPERTY WORD IS REPORTED BEFORE THE OPERATION, because what it says + // determines what a caller above this interface may do. An environment whose + // copy does not carry handles cannot support a library reaching for POSIX + // fork, and stating that is the whole purpose of the position. + { + const bool handles = kal::space::has(kal::space::clone_handles); + const bool deferred = kal::space::has(kal::space::deferred_copy); + line(handles ? " the started context holds the handles the caller holds" + : " the started context holds memory only, not handles"); + line(deferred ? " the copy may be completed lazily" + : " the copy is complete when the call reports success"); + } + + // A CONTEXT STARTED HERE IS A PROCESS, which is why kal_process_wait applies + // to it unchanged rather than being restated in this interface. That it is + // waitable, and that what it reported is what the entry chose, is what is + // observed. A caller cannot observe its stores. + { + int marker = 0; + kal_process p{}; + const int src = kal_space_start(&entry, static_cast(&marker), + child_stack + sizeof child_stack, &p); + if (src != kal_ok) { + unobserved(kind::behaviour, "openkal.space", + "a context could not be started in this environment"); + return; + } + observe(kind::behaviour, true, "a context starts in a copy of the space"); + + int status = 0, terminated = 0; + const int wrc = kal_process_wait(p, &status, &terminated); + observe(kind::behaviour, wrc == kal_ok, + "the started context is waited for as a process"); + observe(kind::behaviour, wrc != kal_ok || terminated == 0, + "the started context ended of its own accord"); + + // THE ENTRY RECEIVED ITS ARGUMENT, observed through the status because + // that is the only channel a separate address space has. The entry + // reports one value for a null argument and another for the pointer it + // was given, so a status of seven states that the argument arrived. + observe(kind::behaviour, wrc != kal_ok || status == 7, + "the entry received the argument it was given"); + + kal_process_close(p); + + // THE CALLER'S OWN MEMORY IS UNCHANGED, AND THAT IS THE POINT. A copy is + // a separate address space; a caller observing a store made by the + // started context would be observing a shared space, which is + // openkal.task and not this interface. + observe(kind::behaviour, marker == 0, + "a store in the copied space is not observed in the original"); + } + + // Starting a second context succeeds after the first has ended, which is + // what a caller running a sequence of them requires. A handle scheme that + // retained the first would fail here rather than at some later count. + { + kal_process p{}; + const int rc = kal_space_start(&entry, nullptr, + child_stack + sizeof child_stack, &p); + if (rc == kal_ok) { + int status = 0, terminated = 0; + kal_process_wait(p, &status, &terminated); + observe(kind::behaviour, status == 1, + "a second context starts and reports its own status"); + kal_process_close(p); + } else { + unobserved(kind::behaviour, "a second context starts", + "the second start was refused"); + } + } +#endif +} + +} diff --git a/conformance/src/sections/space.cppm b/conformance/src/sections/space.cppm new file mode 100644 index 0000000..c526d79 --- /dev/null +++ b/conformance/src/sections/space.cppm @@ -0,0 +1,6 @@ +// The section that examines openkal.space. +export module okc.space; + +export namespace okc::space { +void run(); +} diff --git a/conformance/src/sections/terminal.cpp b/conformance/src/sections/terminal.cpp new file mode 100644 index 0000000..65ebb99 --- /dev/null +++ b/conformance/src/sections/terminal.cpp @@ -0,0 +1,125 @@ +module okc.terminal; + +import openkal.types; +import openkal.stream; +import openkal.terminal; +import okc.report; +import okc.spec; + +namespace okc::terminal { + +void run() { + heading("openkal.terminal"); +#ifndef MCPP_FEATURE_TERMINAL + unobserved(kind::behaviour, "openkal.terminal", "the interface was not selected"); + return; +#else + // THE RESOURCE THIS INTERFACE APPLIES TO IS NOT ALWAYS PRESENT, and the + // suite must not report a failure when it is absent. A run under a pipe has + // no terminal, which is the ordinary case in continuous integration, so + // every observation below is conditioned on finding one and the condition + // is reported rather than assumed. + // The properties are named through the modules and not through the macros: + // a macro does not cross a module boundary, and these sections are modules. + const kal_stream out = kal_stdout(); + const bool interactive = + kal::stream_props{kal_stream_props(out)}.has(kal::stream_prop::interactive); + + if (!interactive) { + unobserved(kind::behaviour, "openkal.terminal", + "no standard stream is interactive in this run"); + } + + // The operations answer for a stream that is not a terminal, and the answer + // is a refusal rather than a silent success. This is observable whether or + // not a terminal is present, and it is the half of the contract that a run + // under a pipe can still examine. + { + kal_uintptr mode = 0; + const int rc = kal_terminal_get_mode(out, &mode); + if (!interactive) { + observe(kind::behaviour, rc == kal_err_not_supported, + "a stream that is not interactive refuses get_mode"); + } else { + observe(kind::behaviour, rc == kal_ok, + "an interactive stream reports its mode"); + } + } + + if (!interactive) return; + + // THE PAIR IS AN INVERSE, AND THAT IS WHAT IS OBSERVED HERE. A get followed + // by a set of what was read leaves the terminal as it was found, which is + // the property clause 7.11 requires and the reason a setter alone would not + // suffice. The mode is restored before the section returns whatever the + // outcome, so that a failing run does not leave the reader's terminal + // without an echo. + kal_uintptr original = 0; + const int got = kal_terminal_get_mode(out, &original); + if (got != kal_ok) { + unobserved(kind::behaviour, "get_mode then set_mode restores", + "the mode could not be read"); + return; + } + + { + const int rc = kal_terminal_set_mode(out, original); + observe(kind::behaviour, rc == kal_ok, + "setting the mode that was read succeeds"); + + kal_uintptr again = 0; + const int re = kal_terminal_get_mode(out, &again); + observe(kind::behaviour, re == kal_ok && again == original, + "the mode read back is the mode that was set"); + } + + // A position this implementation does not distinguish reads as zero and is + // ignored when set; neither is an error. The observation is that an + // unassigned position does not turn a set into a failure, because a program + // compiled against a later revision will set exactly such a position. + { + const kal_uintptr unassigned = (kal_uintptr)1u << 20; + const int rc = kal_terminal_set_mode(out, original | unassigned); + observe(kind::behaviour, rc == kal_ok, + "an unassigned position in the mode word is not an error"); + kal_terminal_set_mode(out, original); + } + + // The display size, where it is known. An environment that cannot ask + // reports not_supported and leaves both outputs untouched, so the outputs + // are pre-set to a value the operation would not produce. + { + kal_uintptr cols = 0xDEAD, rows = 0xBEEF; + const int rc = kal_terminal_size(out, &cols, &rows); + if (rc == kal_ok) { + observe(kind::behaviour, cols > 0 && rows > 0, + "a reported display size is not zero in either dimension"); + } else { + observe(kind::behaviour, + rc == kal_err_not_supported && cols == 0xDEAD && rows == 0xBEEF, + "an unknown display size leaves both outputs untouched"); + } + } + + // Whichever positions the property word claims, the operations that + // correspond to them must be answered. A word claiming a facility the + // implementation refuses is the disagreement clause 6.2 exists to prevent. + { + const auto props = kal::terminal::properties(out); + if (props.has(kal::terminal::has_size)) { + kal_uintptr c = 0, r = 0; + observe(kind::behaviour, kal_terminal_size(out, &c, &r) == kal_ok, + "a claimed display size is reported when asked for"); + } + if (props.has(kal::terminal::has_mode)) { + kal_uintptr m = 0; + observe(kind::behaviour, kal_terminal_get_mode(out, &m) == kal_ok, + "a claimed mode is reported when asked for"); + } + } + + kal_terminal_set_mode(out, original); +#endif +} + +} diff --git a/conformance/src/sections/terminal.cppm b/conformance/src/sections/terminal.cppm new file mode 100644 index 0000000..444fb61 --- /dev/null +++ b/conformance/src/sections/terminal.cppm @@ -0,0 +1,6 @@ +// The section that examines openkal.terminal. +export module okc.terminal; + +export namespace okc::terminal { +void run(); +} diff --git a/conformance/src/sections/timeout.cpp b/conformance/src/sections/timeout.cpp new file mode 100644 index 0000000..a6c689c --- /dev/null +++ b/conformance/src/sections/timeout.cpp @@ -0,0 +1,70 @@ +module okc.timeout; + +import openkal.types; +import openkal.stream; +import openkal.timeout; +import okc.report; +import okc.spec; + +namespace okc::timeout { + +void run() { + heading("openkal.timeout"); +#ifndef MCPP_FEATURE_TIMEOUT + unobserved(kind::behaviour, "openkal.timeout", "the interface was not selected"); + return; +#else + claim("kal_timeout_granularity_ns", kal_timeout_granularity_ns); + + // A GRANULARITY OF ZERO WOULD BE A CLAIM NO CLOCK CAN MEET. The word states + // the smallest bound the implementation distinguishes, and an implementation + // reporting zero would be asserting an infinitely fine clock rather than + // declining to answer. + observe(kind::behaviour, kal_timeout_granularity_ns > 0, + "the granularity is a positive number of nanoseconds"); + + // AN EXPIRED BOUND IS kal_err_again AND NOT A NEW ERROR VALUE. The error set + // is closed (clause 5.2), and "the operation would block" is what an expiry + // is. Observed on a read from the standard input, which in a run without a + // terminal has nothing to give and must therefore expire rather than wait. + // + // THE ZERO-LENGTH CASE IS SEPARATED FROM THE EXPIRY, because a read of zero + // bytes succeeds trivially and would report kal_ok whatever the bound. + { + char buf[1] = {0}; + const kal_io_result r = kal_timeout_read(kal_stdin(), buf, sizeof buf, 1); + observe(kind::behaviour, + r.e == kal_ok || r.e == kal_err_again || r.e == kal_err_not_supported, + "a bounded read reports success, an expiry, or a refusal"); + if (r.e == kal_err_again) + observe(kind::behaviour, r.n == 0, + "an expired read transferred nothing"); + } + + // A BOUND OF ZERO IS NO BOUND, which is the convention kal_task_wait already + // establishes and the reason this interface states a duration rather than an + // instant. It is not observed by waiting --- a run that blocked would never + // report --- but by writing, which does not wait. + { + const kal_io_result r = kal_timeout_write(kal_stdout(), "", 0, 0); + observe(kind::behaviour, r.e == kal_ok || r.e == kal_err_not_supported, + "a bound of zero is accepted and denotes no bound"); + } + + // An implementation may provide this interface for some of its resource + // types and not others, and reports kal_err_not_supported for the rest. + // What is not conforming is reporting success having waited without bound. + // That cannot be observed in finite time, so what is observed instead is + // that the refusal, where it is given, is the defined one. + { + char buf[1] = {0}; + const kal_io_result r = kal_timeout_read(kal_stdin(), buf, sizeof buf, 1000000); + observe(kind::behaviour, + r.e == kal_ok || r.e == kal_err_again || + r.e == kal_err_not_supported || r.e == kal_err_io, + "a refusal is drawn from the closed error set"); + } +#endif +} + +} diff --git a/conformance/src/sections/timeout.cppm b/conformance/src/sections/timeout.cppm new file mode 100644 index 0000000..1afeb55 --- /dev/null +++ b/conformance/src/sections/timeout.cppm @@ -0,0 +1,6 @@ +// The section that examines openkal.timeout. +export module okc.timeout; + +export namespace okc::timeout { +void run(); +} diff --git a/conformance/src/spec.cppm b/conformance/src/spec.cppm index 98348e8..a96de0a 100644 --- a/conformance/src/spec.cppm +++ b/conformance/src/spec.cppm @@ -39,7 +39,8 @@ constexpr const char* name_of(kind k) { // them in, and the report follows it, so two runs of different implementations // can be read side by side. enum class interface_id { - abort, stream, memory, env, time, fs, process, task, count + abort, stream, memory, env, time, fs, process, task, + terminal, net, datagram, space, timeout, count }; struct interface_row { @@ -110,6 +111,41 @@ inline constexpr interface_row inventory[] = { false, #endif "task" }, + { "openkal.terminal", false, +#ifdef MCPP_FEATURE_TERMINAL + true, +#else + false, +#endif + "terminal" }, + { "openkal.net", false, +#ifdef MCPP_FEATURE_NET + true, +#else + false, +#endif + "net" }, + { "openkal.datagram", false, +#ifdef MCPP_FEATURE_DATAGRAM + true, +#else + false, +#endif + "datagram" }, + { "openkal.space", false, +#ifdef MCPP_FEATURE_SPACE + true, +#else + false, +#endif + "space" }, + { "openkal.timeout", false, +#ifdef MCPP_FEATURE_TIMEOUT + true, +#else + false, +#endif + "timeout" }, }; // Which kinds of examination this build performs. Behaviour is unconditional: diff --git a/conformance/src/suite.cpp b/conformance/src/suite.cpp index bb5fc94..e51c460 100644 --- a/conformance/src/suite.cpp +++ b/conformance/src/suite.cpp @@ -12,6 +12,11 @@ import okc.fs; import okc.process; import okc.task; import okc.exec; +import okc.terminal; +import okc.net; +import okc.datagram; +import okc.space; +import okc.timeout; namespace okc { @@ -32,6 +37,11 @@ int run_all() { process::run(); task::run(); exec::run(); + terminal::run(); + net::run(); + datagram::run(); + space::run(); + timeout::run(); termination::run(); return summarise(); diff --git a/examples/portable/README.md b/examples/portable/README.md index 86c5e86..ed57de2 100644 --- a/examples/portable/README.md +++ b/examples/portable/README.md @@ -4,11 +4,15 @@ One program that exercises the eight interfaces the specification defines. It names no implementation, no operating system, no descriptor number and no system call. -Its purpose is to be built by every implementation. An implementation's -continuous integration fetches this source from the specification repository at -the version its manifest names, builds it against itself, and asserts the lines -below. The source is therefore identical everywhere by construction rather than -by a copy that can drift. +Its purpose is to be built over every implementation. The specification's own +continuous integration does that: the conformance job builds this source against +each implementation it tests and requires the final count to be zero, so the one +source is exercised over all of them rather than copied into each. + +That sentence used to describe the implementations' own workflows doing it. None +of them did, and the manifest here had drifted three specification versions +behind without anything noticing --- which is what a claim with no criterion +behind it costs. It prints one line per interface, each beginning `openkal: `, and a final line reporting the number of observations that did not hold. An implementation diff --git a/examples/portable/mcpp.toml b/examples/portable/mcpp.toml index bd7fe77..181dd21 100644 --- a/examples/portable/mcpp.toml +++ b/examples/portable/mcpp.toml @@ -9,13 +9,13 @@ name = "portable" version = "0.1.0" [dependencies] -openkal = "0.5.0" +openkal = "0.8.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.5.0" +openkal-linux = "0.6.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.3.0" +openkal-macos = "0.4.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.1.0" +openkal-windows = "0.2.0" diff --git a/examples/portable/src/main.cpp b/examples/portable/src/main.cpp index 9080911..d7aa039 100644 --- a/examples/portable/src/main.cpp +++ b/examples/portable/src/main.cpp @@ -4,6 +4,26 @@ // Every implementation builds this same source and asserts the same lines. The // source is fetched from the specification repository rather than copied into // each implementation, so the programs cannot diverge. +// +// ⚠️⚠️ NAMING NO SYSTEM CALL IS NOT THE SAME AS NAMING NO COMPILER. +// +// This file used `__atomic_load_n`, `__ATOMIC_RELAXED` and `__UINT32_TYPE__`, +// which are GCC and Clang spellings. MSVC has none of them, so the program +// whose whole premise is portability did not compile on one of the three +// compilers its own specification is tested against --- and nothing noticed, +// because until 2026-08-27 no continuous integration anywhere built this file. +// +// The integer types are now the specification's own: openkal/types.h already +// carries the MSVC branch, so `kal_u32` and `kal_u64` are the portable +// spellings and using them keeps this program inside the vocabulary it exists +// to demonstrate. +// +// The atomics are the LANGUAGE's. `std::atomic_ref` is freestanding in C++23, +// so depending on it does not contradict what this program shows: suspension +// is a kernel facility and openkal offers it, while an atomic operation is a +// language facility and openkal is right not to. +#include + import openkal.types; import openkal.stream; import openkal.memory; @@ -50,26 +70,31 @@ static void observe(bool held, const char* what) { // A mutex is not part of the specification. It is constructed here from the // primitive that is, which is the relation a C library has to a kernel. -static __UINT32_TYPE__ lock_state = 0; // 0 free, 1 held, 2 held and contended +static kal_u32 lock_state = 0; // 0 free, 1 held, 2 held and contended static unsigned long long counter = 0; -static bool exchange(__UINT32_TYPE__* p, __UINT32_TYPE__ expected, __UINT32_TYPE__ desired) { - return __atomic_compare_exchange_n(p, &expected, desired, false, - __ATOMIC_ACQUIRE, __ATOMIC_ACQUIRE); +// The word is waited upon through openkal and modified through the language, so +// both must see the same object. `atomic_ref` refers to it rather than +// replacing it, which is what lets `kal_task_wait` take its address. +static bool exchange(kal_u32* p, kal_u32 expected, kal_u32 desired) { + return std::atomic_ref(*p).compare_exchange_strong( + expected, desired, std::memory_order_acquire, std::memory_order_acquire); } static void acquire() { if (exchange(&lock_state, 0, 1)) return; do { - __UINT32_TYPE__ seen = __atomic_load_n(&lock_state, __ATOMIC_RELAXED); + const kal_u32 seen = + std::atomic_ref(lock_state).load(std::memory_order_relaxed); if (seen == 2 || exchange(&lock_state, 1, 2)) kal_task_wait(&lock_state, 2, 0); } while (!exchange(&lock_state, 0, 2)); } static void release() { - if (__atomic_fetch_sub(&lock_state, 1, __ATOMIC_RELEASE) != 1) { - __atomic_store_n(&lock_state, 0, __ATOMIC_RELEASE); + if (std::atomic_ref(lock_state) + .fetch_sub(1, std::memory_order_release) != 1) { + std::atomic_ref(lock_state).store(0, std::memory_order_release); kal_uintptr woken = 0; kal_task_wake(&lock_state, 1, &woken); } @@ -144,7 +169,7 @@ int main() { kal_file g{}; bool sought = false; if (made && kal_fs_open_file(wd, "portable.probe", 14, 0, 0, &g) == kal_ok) { - __UINT64_TYPE__ at = 0; + kal_u64 at = 0; sought = kal_fs_seek(g, 6, kal::fs::seek_set, &at) == kal_ok && at == 6; char buf[8] = {}; kal_io_result r = kal_stream_read(kal_stream{kal_fs_stream(g)}, buf, 4); diff --git a/include/openkal.h b/include/openkal.h index 2995ad0..36a0d75 100644 --- a/include/openkal.h +++ b/include/openkal.h @@ -22,5 +22,12 @@ #include "openkal/process.h" #include "openkal/task.h" #include "openkal/exec.h" +#include "openkal/terminal.h" +#include "openkal/net.h" +#include "openkal/datagram.h" +#include "openkal/space.h" +/* Last, because its declarations are the operations of the interfaces above + * with one argument added, and it includes each of them. */ +#include "openkal/timeout.h" #endif /* OPENKAL_H */ diff --git a/include/openkal/datagram.h b/include/openkal/datagram.h new file mode 100644 index 0000000..e330923 --- /dev/null +++ b/include/openkal/datagram.h @@ -0,0 +1,74 @@ +/* openkal.datagram --- messages with boundaries, sent without a connection. + * + * A SEPARATE INTERFACE FROM `openkal.net', for the reason clause 6.4 gives: a + * datagram and a connection are two resources, and an operation that some + * resources of an interface can never satisfy does not belong in it. A message + * boundary is not a property a byte stream has; ordering is not a property a + * datagram has. Merging them would produce exactly the interface this + * specification's decomposition exists to avoid. + * + * AND IT IS THE EASIER HALF TO PROVIDE. A board that carries an IP stack + * reaches datagrams in a few hundred lines and connections in a few thousand, + * so an implementation supplying only this one is ordinary rather than + * deficient --- clause 6.1 already expresses that by absence, and no capability + * word is needed to say it. */ +#ifndef OPENKAL_DATAGRAM_H +#define OPENKAL_DATAGRAM_H +#include "types.h" + +/* An opaque handle occupying one machine word, per clause 7.2. + * + * NOT A STREAM. `kal_stream_read' reports a count and not a boundary, so a + * datagram read through it would lose the one property that distinguishes this + * interface. The handle is its own type so that the mistake cannot be made. */ +struct kal_datagram { kal_uintptr h; }; + +/* Positions in kal_datagram_props. */ +#define KAL_DGRAM_PROP_IPV6 ((kal_uintptr)1u << 0) +#define KAL_DGRAM_PROP_BROADCAST ((kal_uintptr)1u << 1) + +#ifdef __cplusplus +extern "C" { +#endif + +/* Opens a datagram endpoint. + * + * A local endpoint whose port is zero asks the environment to choose one; a + * caller that must publish where it is reads it back with + * `kal_datagram_local'. A null local endpoint asks for one that may send and + * whose receiving address is unspecified. */ +int kal_datagram_open(const struct kal_endpoint* local, struct kal_datagram* d); + +/* Reports the endpoint this one was given, for the reason given above and + * under clause 7.11. */ +int kal_datagram_local(struct kal_datagram d, struct kal_endpoint* out); + +/* Sends one message. + * + * A message is sent whole or not at all; a partial send is not a result this + * interface produces. The count reported on success is therefore always the + * length that was given, and is reported so that the result type is the one + * every transferring operation in openkal uses. */ +struct kal_io_result kal_datagram_send_to(struct kal_datagram d, + const void* buf, kal_uintptr len, + const struct kal_endpoint* to); + +/* Reports one message and who sent it. + * + * A message longer than the buffer is truncated and the excess is lost, which + * is what the medium does. The count reported is what was placed in the buffer; + * a caller that must not lose bytes offers a buffer as large as the largest + * message it will accept. */ +struct kal_io_result kal_datagram_recv_from(struct kal_datagram d, + void* buf, kal_uintptr len, + struct kal_endpoint* from); + +void kal_datagram_close(struct kal_datagram d); + +extern const kal_uintptr kal_datagram_props; + +#ifdef __cplusplus +} +#endif + +#endif /* OPENKAL_DATAGRAM_H */ diff --git a/include/openkal/net.h b/include/openkal/net.h new file mode 100644 index 0000000..911022c --- /dev/null +++ b/include/openkal/net.h @@ -0,0 +1,103 @@ +/* openkal.net --- a connection, which is a stream with a peer and a way to be + * half-closed. + * + * NOT MERGED WITH `openkal.fs', and clause 3.4 records why: positioning applies + * to a file and not to a connection, half-closure to a connection and not to a + * file. Two interfaces whose operations do not both apply to both resources are + * two interfaces. + * + * A connection IS a stream once established --- `kal_stream_read' and + * `kal_stream_write' are the operations that move its bytes, and this interface + * adds none of its own. What is here is what a connection has and a stream in + * general does not: a peer, an accept, and an end that can be closed in one + * direction. + * + * NAME RESOLUTION IS NOT HERE. Clause 3.4 excludes it in terms: an + * implementation shall not be required to parse an unbounded set of name + * schemes. An endpoint is an address and a port; turning "example.com" into one + * is a library above this interface and `openkal.datagram'. */ +#ifndef OPENKAL_NET_H +#define OPENKAL_NET_H +#include "types.h" +#include "stream.h" + +/* Opaque handles occupying one machine word, per clause 7.2. + * + * A CONNECTION IS AN OWNED HANDLE AND ITS STREAM IS BORROWED FROM IT, WHICH IS + * THE ARRANGEMENT `openkal.fs' ALREADY USES. An earlier form of this interface + * yielded a `kal_stream' directly and declared it owned. That could not be + * implemented: clause 7.2 requires that a released handle not be treated as + * valid, and a stream handle is whatever the environment's transfer operations + * take --- on a system of descriptors, a number the environment reuses as soon + * as it is closed. An owned handle can carry a generation beside the resource + * and a borrowed stream cannot, so the two are separate here exactly as + * `kal_file' and `kal_fs_stream' are separate there. + * + * A listener is neither: nothing is transferred through it, and giving it the + * stream operations would be an interface whose resource can never satisfy + * them. */ +struct kal_net_conn { kal_uintptr h; }; +struct kal_net_listener { kal_uintptr h; }; + +/* Directions for kal_net_shutdown. */ +#define KAL_SHUT_READ 1 +#define KAL_SHUT_WRITE 2 +#define KAL_SHUT_BOTH 3 + +/* Positions in kal_net_props. */ +#define KAL_NET_PROP_IPV6 ((kal_uintptr)1u << 0) +#define KAL_NET_PROP_HALFCLOSE ((kal_uintptr)1u << 1) + +#ifdef __cplusplus +extern "C" { +#endif + +int kal_net_connect(const struct kal_endpoint* to, struct kal_net_conn* out); +int kal_net_listen (const struct kal_endpoint* local, struct kal_net_listener* out); +int kal_net_accept (struct kal_net_listener l, struct kal_net_conn* out); + +/* A connection is read and written through openkal.stream. The stream remains + * valid while the connection is open and is not separately released; the + * connection owns it. The wording is `openkal.fs's, because the arrangement is. */ +kal_uintptr kal_net_stream(struct kal_net_conn c); + +/* Reports the endpoint of the peer, and the endpoint this end was given. + * + * The second is not the one that was asked for: a listener opened on port zero + * is given a port by the environment, and a program that must publish where it + * is listening has no other way to learn it. Clause 7.11 --- an enquiry whose + * inverse exists --- is why both are here rather than only the first. */ +int kal_net_peer (struct kal_net_conn c, struct kal_endpoint* out); +int kal_net_local (struct kal_net_conn c, struct kal_endpoint* out); +int kal_net_listener_local(struct kal_net_listener l, struct kal_endpoint* out); + +/* Ends transfer in one direction while the other continues. + * + * This is the operation that distinguishes a connection from a file, and it is + * why the two are separate interfaces. An implementation that cannot express it + * reports kal_err_not_supported and withholds KAL_NET_PROP_HALFCLOSE; a caller + * that needs the peer to observe end-of-input must then close the whole + * connection. */ +int kal_net_shutdown(struct kal_net_conn c, int direction); + +/* Release. An implementation shall not treat a released handle as valid. + * + * The stream a released connection yielded is released with it and is not + * closed separately, which is the division `openkal.fs' draws with + * `kal_fs_close_file' and the reason no release lives in `openkal.stream': a + * stream in general has no owner to return it to. */ +void kal_net_close (struct kal_net_conn c); +void kal_net_close_listener(struct kal_net_listener l); + +/* Properties of this implementation. + * + * A word rather than an enquiry, because these do not vary between the + * resources of the interface: an implementation either speaks IPv6 or does not + * (clause 6.2). */ +extern const kal_uintptr kal_net_props; + +#ifdef __cplusplus +} +#endif + +#endif /* OPENKAL_NET_H */ diff --git a/include/openkal/process.h b/include/openkal/process.h index 6cbd28c..6b48de5 100644 --- a/include/openkal/process.h +++ b/include/openkal/process.h @@ -7,6 +7,10 @@ #define OPENKAL_PROCESS_H #include "types.h" #include "fs.h" +/* For `kal_process_channel', whose pair of streams is what a parent speaks to + * a started program through. A sibling header, per the rule this specification + * keeps for its own headers. */ +#include "stream.h" struct kal_process { kal_uintptr h; }; @@ -20,10 +24,20 @@ struct kal_spawn_streams { kal_uintptr err; }; +/* One directory a started program shall receive among its preopens. The layout + * is frozen (clause 5.3). */ +struct kal_preopen { + struct kal_dir dir; + const char* name; + kal_uintptr len; +}; + /* Positions in kal_process_props. */ #define KAL_PROCESS_PROP_TERMINATE ((kal_uintptr)1u << 0) #define KAL_PROCESS_PROP_STREAM_PASSING ((kal_uintptr)1u << 1) #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) #ifdef __cplusplus extern "C" { @@ -40,6 +54,40 @@ int kal_process_spawn(struct kal_dir base, const struct kal_spawn_streams* streams, struct kal_process* out); +/* Starts a program that receives exactly the directories named. + * + * The started program reads them back through `kal_fs_preopen', which is the + * operation this one is the inverse of --- clause 7.11. A count of zero starts + * a program with no preopens at all, which is a different thing from + * `kal_process_spawn' and is the whole reason a caller reaches for this. + * + * A SECOND DECLARATION RATHER THAN AN ARGUMENT ADDED TO THE FIRST, because + * clause 8 forbids altering an existing one. `kal_process_spawn' remains, and a + * program that does not grant directories keeps using it. */ +int kal_process_spawn_with(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, + const struct kal_preopen* grants, kal_uintptr grant_count, + struct kal_process* out); + +/* A pair of streams of which one end is intended to cross a spawn boundary. + * + * The caller holds `mine'; `theirs' is what it places in a + * `kal_spawn_streams'. Bytes written to one end are read from the other, and + * the pair is the mechanism a parent uses to speak to a child it started --- + * which openkal otherwise has no way to express, because the standard streams + * are borrowed and cannot be manufactured. + * + * BOTH ENDS ARE OWNED, AND THE RELEASE IS HERE. A parent that does not release + * `theirs' after the spawn never observes the end of input on `mine' --- the + * classic deadlock of this arrangement --- so an interface that hands out these + * streams must also take them back. `kal_stream' has no release of its own + * precisely because a stream in general has no owner. */ +int kal_process_channel(struct kal_stream* mine, struct kal_stream* theirs); +void kal_process_channel_close(struct kal_stream s); + 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); diff --git a/include/openkal/space.h b/include/openkal/space.h new file mode 100644 index 0000000..22a36c0 --- /dev/null +++ b/include/openkal/space.h @@ -0,0 +1,86 @@ +/* openkal.space --- starting a context in a copy of the calling address space. + * + * THIS IS NOT `fork'. Clause 7.1 refuses to require the duplication of an + * address space AND ITS EXECUTION STATE; what is here is the first half alone. + * The started context begins at a function the caller names, not at the + * instruction the caller was executing --- which is what lets this be stated in + * a C application binary interface at all. + * + * A library above this interface reaches `fork' by saving its own execution + * state before the call and restoring it in the started context. That is + * composition, and it belongs above this line rather than in it: the saving is + * done with the compiler's own facilities, differs per architecture, and is not + * something a kernel interface can perform on a caller's behalf. + * + * ONE OPERATION AND NOT TWO, AND CLAUSE 7.1 IS WHY. An earlier form of this + * interface separated the copying of the space from the starting of a context + * in it, so that a caller held a space as a handle. No environment this + * specification targets has that pair as a primitive: the copy and the start are + * one act, and an implementation asked to separate them would have to start a + * context anyway, park it upon a waiting primitive, and build a channel by which + * to tell it what to run. That is a mechanism reconstructed rather than a + * facility conveyed, which clause 7.1 identifies as a fault in the shape of the + * specification rather than in the implementation. Clause 6.3 records the + * separated form among the mechanisms considered and not adopted. + * + * THERE IS NO OPERATION THAT CREATES AN EMPTY SPACE. An empty address space + * contains no code, so the entry function a caller would name is not in it. A + * program that wants a child holding only what it grants uses + * `kal_process_spawn_with', which starts a named program rather than a function + * of the caller's. */ +#ifndef OPENKAL_SPACE_H +#define OPENKAL_SPACE_H +#include "types.h" +#include "process.h" + +/* Positions in kal_space_props. */ + +/* Whether the started context holds the handles the caller holds. + * + * An environment whose copying primitive carries memory and not handles reports + * zero here, and a library above it cannot reach POSIX fork semantics. Stating + * it lets that library refuse at the point of the attempt rather than produce a + * child that is subtly not the one POSIX describes. */ +#define KAL_SPACE_PROP_CLONE_HANDLES ((kal_uintptr)1u << 0) + +/* Whether the copy may be completed lazily, so that a store to copied memory may + * fail after the operation has already reported success. + * + * A program that cannot tolerate a deferred failure adapts to this; it cannot + * ask for the other behaviour. The word says which environment the program is + * in, and nothing here promises to change it: an implementation whose + * environment defers the copy cannot undefer it, and one that does not cannot + * pretend to. */ +#define KAL_SPACE_PROP_DEFERRED_COPY ((kal_uintptr)1u << 1) + +#ifdef __cplusplus +extern "C" { +#endif + +/* Starts a context in a copy of the calling address space. + * + * The result is a process: a context with an address space of its own is what + * that word means, so `kal_process_wait', `kal_process_terminate' and + * `kal_process_close' apply to it unchanged rather than being restated here. + * + * The copy is taken at this call. What the caller stores afterwards is not seen + * by the started context, and what the started context stores is not seen by the + * caller; two contexts sharing an address space is `openkal.task' and not this + * interface. + * + * The stack is the caller's to provide, and its top is passed rather than its + * base because that is the end execution begins from on every architecture this + * specification targets. An implementation whose environment gives the started + * context a stack of its own ignores the argument, and reports that by + * withholding nothing: a caller cannot observe which of the two occurred, and + * has no decision resting upon it. */ +int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, + struct kal_process* out); + +extern const kal_uintptr kal_space_props; + +#ifdef __cplusplus +} +#endif + +#endif /* OPENKAL_SPACE_H */ diff --git a/include/openkal/terminal.h b/include/openkal/terminal.h new file mode 100644 index 0000000..e049759 --- /dev/null +++ b/include/openkal/terminal.h @@ -0,0 +1,74 @@ +/* openkal.terminal --- what an interactive stream does with what is typed at + * it. + * + * The resource is a stream for which `kal_stream_props' reports + * KAL_STREAM_PROP_INTERACTIVE. Every operation here reports + * kal_err_not_supported for any other stream. + * + * A SEPARATE INTERFACE RATHER THAN OPERATIONS UPON `openkal.stream', for the + * reason clause 6.4 gives when it places positioning in `openkal.fs': the + * behaviour varies between the RESOURCES of the stream interface rather than + * between implementations. An implementation could neither claim these + * operations honestly for a file nor withhold them usefully for a terminal, and + * an interface containing an operation some of its resources can never satisfy + * is the defect this specification's decomposition exists to avoid. + * + * THE PAIR IS get/set AND NOT TWO SETTERS. A program that turns line editing + * off must be able to put back what was there, and a setter alone gives it + * nothing to put back --- it would restore a default, and the terminal a user + * returns to is then not the one they had. Clause 7.11 states the general rule + * that an enquiry has an inverse; this is an instance of it. */ +#ifndef OPENKAL_TERMINAL_H +#define OPENKAL_TERMINAL_H +#include "types.h" +#include "stream.h" + +/* Positions in the mode word. + * + * A position that has not been assigned reads as zero, so a program compiled + * against a later revision of this specification behaves correctly against an + * earlier implementation (clause 6.2). */ +#define KAL_TERM_LINE_EDIT ((kal_uintptr)1u << 0) /* the environment assembles lines */ +#define KAL_TERM_ECHO ((kal_uintptr)1u << 1) /* the environment shows what is typed */ + +/* Positions in the result of kal_terminal_props. */ +#define KAL_TERM_PROP_MODE ((kal_uintptr)1u << 0) /* get_mode/set_mode are answered */ +#define KAL_TERM_PROP_SIZE ((kal_uintptr)1u << 1) /* the display size is known */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Reports the mode currently in effect, and establishes a mode. + * + * A position this implementation does not distinguish is reported as zero by + * the first and ignored by the second; neither is an error. An implementation + * that distinguishes no position at all withholds the whole interface instead + * (clause 6.1), so that a program discovers the absence at the link rather than + * at a call that reports success having done nothing. */ +int kal_terminal_get_mode(struct kal_stream s, kal_uintptr* mode); +int kal_terminal_set_mode(struct kal_stream s, kal_uintptr mode); + +/* The size of the display, in character cells. + * + * An environment that does not know --- a serial line has no way to ask --- + * reports kal_err_not_supported and leaves both outputs untouched. + * + * THERE IS NO NOTIFICATION. openkal has no signals, so a program learns of a + * change by asking again. A program with an event loop already has somewhere to + * ask from; one without does not need to know. */ +int kal_terminal_size(struct kal_stream s, kal_uintptr* cols, kal_uintptr* rows); + +/* Properties of one terminal. + * + * An enquiry rather than a word, for the same reason `kal_stream_props' is one: + * the answer varies between the resources of the interface and not between + * implementations. The same implementation answers differently for a pseudo + * terminal and for a serial line. */ +kal_uintptr kal_terminal_props(struct kal_stream s); + +#ifdef __cplusplus +} +#endif + +#endif /* OPENKAL_TERMINAL_H */ diff --git a/include/openkal/timeout.h b/include/openkal/timeout.h new file mode 100644 index 0000000..a157490 --- /dev/null +++ b/include/openkal/timeout.h @@ -0,0 +1,63 @@ +/* openkal.timeout --- a bound upon operations that would otherwise wait + * without end. + * + * Every operation here is the operation of the same name in another interface, + * with one argument added. Clause 7.8 already establishes that a second form of + * one operation is admissible when the first cannot state the whole of an + * intent: `kal_fs_open_file' and `kal_fs_open' stand beside each other for that + * reason, and these stand beside their originals for the same one. + * + * THE ARGUMENT IS A DURATION, NOT AN INSTANT, and the name of this interface + * says so. `kal_task_wait' already takes a `timeout_ns' and already defines zero + * as no timeout; a second spelling of the same idea would be the one thing this + * specification most consistently refuses. A caller that holds a deadline + * subtracts the current time itself, which it can do with `openkal.time'. + * + * An expired bound is reported as `kal_err_again' --- "the operation would + * block" --- which is what an expiry is. The error set is closed (clause 5.2) + * and required no addition. + * + * AN IMPLEMENTATION MAY PROVIDE THIS FOR SOME OF ITS RESOURCES AND NOT OTHERS, + * and reports `kal_err_not_supported' for the rest. That is not the defect + * clause 6.4 describes: the operations are stated once per resource TYPE here, + * so an implementation withholding the datagram form still answers the stream + * form honestly. */ +#ifndef OPENKAL_TIMEOUT_H +#define OPENKAL_TIMEOUT_H +#include "types.h" +#include "stream.h" +#include "net.h" +#include "datagram.h" +#include "process.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct kal_io_result kal_timeout_read (struct kal_stream s, void* buf, kal_uintptr len, kal_u64 timeout_ns); +struct kal_io_result kal_timeout_write(struct kal_stream s, const void* buf, kal_uintptr len, kal_u64 timeout_ns); + +int kal_timeout_accept(struct kal_net_listener l, kal_u64 timeout_ns, struct kal_net_conn* out); + +struct kal_io_result kal_timeout_recv_from(struct kal_datagram d, void* buf, kal_uintptr len, + struct kal_endpoint* from, kal_u64 timeout_ns); + +int kal_timeout_wait_process(struct kal_process p, kal_u64 timeout_ns, + int* status, int* terminated); + +/* The smallest bound this implementation distinguishes, in nanoseconds. + * + * A board whose only clock ticks at a millisecond reports 1000000. A caller + * that asks for less is not refused and does not get less --- the bound is a + * bound, and rounding it up is the only honest answer an environment with a + * coarse clock can give. + * + * Per implementation rather than per resource, so a word rather than an enquiry + * (clause 6.2). */ +extern const kal_uintptr kal_timeout_granularity_ns; + +#ifdef __cplusplus +} +#endif + +#endif /* OPENKAL_TIMEOUT_H */ diff --git a/include/openkal/types.h b/include/openkal/types.h index ad88e8b..543ceb6 100644 --- a/include/openkal/types.h +++ b/include/openkal/types.h @@ -54,6 +54,20 @@ typedef signed __int64 kal_i64; # error "openkal requires a compiler that states a sixty-four bit type" #endif +/* A byte. + * + * The three widths above are the ones openkal's OPERATIONS use --- a size, an + * offset, a count. An address is the first datum this specification must state + * byte by byte rather than as a number, because its meaning is the sequence of + * its bytes and not their arithmetic value. */ +#if defined(__UINT8_TYPE__) +typedef __UINT8_TYPE__ kal_u8; +#elif defined(_MSC_VER) +typedef unsigned char kal_u8; +#else +# error "openkal requires a compiler that states an eight bit type" +#endif + #ifdef __cplusplus extern "C" { #endif @@ -89,6 +103,32 @@ struct kal_io_result { int e; }; +/* Where a connection or a message goes. The layout is frozen (clause 5.3). + * + * HERE AND NOT IN net.h, because `openkal.datagram' uses it too and either + * interface may be provided without the other. A type shared by two interfaces + * belongs to neither of them. + * + * BYTES AND A LENGTH, not a tagged union of families. The length is what + * distinguishes one kind of address from another, and it is a VALUE rather than + * a layout --- so the set of lengths this specification defines may grow while + * clause 5.3 continues to hold the structure itself fixed. That is the rule + * clause 6.2 gives a property word, applied to a value instead of to a bit. + * + * An implementation refuses a length it does not know (kal_err_invalid) rather + * than reading it as one it does. Twenty-four bytes is chosen so that an + * address carrying a scope identifier fits without a second type. */ +struct kal_endpoint { + kal_u8 addr[24]; /* network order --- the bytes of the address */ + kal_uintptr addr_len; /* 4 = IPv4 16 = IPv6 20 = IPv6 with a scope + * identifier. Further values are defined by later + * revisions and are refused, not misread, by an + * implementation that does not know them. */ + kal_u32 port; /* a number, in host order: this interface does + * not ask a caller to perform a protocol's byte + * order conversion */ +}; + #ifdef __cplusplus } #endif diff --git a/kit/mcpp.toml b/kit/mcpp.toml new file mode 100644 index 0000000..1839cfe --- /dev/null +++ b/kit/mcpp.toml @@ -0,0 +1,79 @@ +[package] +namespace = "mcpplibs" +name = "openkal-kit" +version = "0.1.0" +description = "Facilities composed from openkal's interfaces. Not part of the specification, and structurally incapable of being mistaken for it." +license = "Apache-2.0" +authors = ["mcpplibs"] +repo = "https://github.com/mcpplibs/openkal" + +# WHY THIS IS IN THE SPECIFICATION'S REPOSITORY AND IS NOT THE SPECIFICATION. +# +# The specification admits an interface only when it is a minimal capability +# every kernel has and cannot be composed from the interfaces already there. +# That rule is what keeps openkal implementable on a machine with firmware and +# nothing else, and it leaves a gap: a program that wants to await two streams +# at once has an answer in POSIX and no answer here, because awaiting two +# streams is composed rather than primitive. +# +# The gap was being filled by the port layer. There is one port layer today and +# what it composes is POSIX, so a native openkal program either wrote the +# composition again or took a whole C library. This package is where the +# composition is written once. +# +# ⭐ THE CONTRACT FORM IS WHAT MAKES IT UNMISTAKABLE. +# +# Clause 10 states that openkal's contract is a C application binary interface. +# This package deliberately is not one: it is C++ modules in `namespace kal::kit` +# and it exports no name beginning with `kal_`. Measured on its objects --- the +# defined names are C++ mangled module initialisers such as +# `_ZGIW7openkalW3kitW7channel`, and the operations themselves are inline and +# emitted into consumers rather than exported at all. So +# `tools/check-surface.sh --complete` does not read a program linking this +# package as an implementation that has added names: the rule it enforces is +# about the C surface, and this package has none. +# +# ⚠️ THE MEASUREMENT MATTERS BECAUSE THE ARGUMENT IS STRUCTURAL. A claim that +# rests on how a compiler mangles names is worth checking against what the +# compiler actually emitted, and the first form of this comment named a mangling +# (`_ZN3kal3kit…`) these objects do not contain. +# +# So "is this normative" is answered by the shape of what is exported rather +# than by a sentence saying it is not. A sentence can be overlooked; a mangled +# name cannot become a C symbol. +# +# The consequence is the one that matters: this package MAY evolve. Clause 8 +# forbids the specification from altering a declaration it has published, which +# is what makes openkal safe to depend upon and what makes it the wrong place +# for a facility still finding its shape. +[targets.openkal-kit] +kind = "lib" + +[dependencies] +openkal = { path = ".." } + +[build] +include_dirs = ["../include"] + +# AN IMPLEMENTATION, FOR THE TESTS AND FOR NOTHING ELSE. +# +# The package above depends upon the specification and no implementation, which +# is what a consumer of openkal does and what keeps this package usable wherever +# openkal is. The specification declares and does not define, so a LIBRARY built +# from it correctly has undefined references and a TEST cannot. +# +# `mcpp build` ignores this section and `mcpp test` resolves it, so the +# distinction is expressed rather than described: a consumer of this package +# never acquires an implementation through it, and the tests link. +# +# Which implementation is a property of where the tests run and not of this +# package, so it is conditional on the platform exactly as openkal-musl's own +# manifest makes its backend conditional. +# Named by version rather than by path, because a path here would say that the +# implementation lives beside this repository, which is true on one machine and +# on no other. The workflow that runs these tests substitutes a working tree, as +# tools/run-conformance.sh already does for the conformance suite --- and for the +# 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.6.0", features = ["standalone"] } diff --git a/kit/src/channel.cppm b/kit/src/channel.cppm new file mode 100644 index 0000000..0914396 --- /dev/null +++ b/kit/src/channel.cppm @@ -0,0 +1,189 @@ +// openkal.kit.channel --- bytes carried from one context to another, inside one +// address space. +// +// WHY THIS IS NOT AN INTERFACE OF THE SPECIFICATION. Every part of it is +// composed from what openkal already has: a region from openkal.memory, and +// openkal.task's wait and wake to make a reader sleep rather than spin. A +// facility that can be written in terms of the atoms is one the specification +// declines to admit, and this module is where such a facility goes instead. +// +// It is deliberately NOT `kal_process_channel'. That interface carries a stream +// across a spawn boundary and is a kernel facility because a child process +// cannot be handed a pointer into its parent's memory. This one is within one +// address space, needs nothing from the kernel but a place to sleep, and is +// therefore the composed half of the same idea. +module; +#include +#include +#include + +export module openkal.kit.channel; + +import openkal.types; + +namespace kal::kit::detail { + +// A power of two, so that the wrap is a mask rather than a division. A division +// on every byte is what a ring buffer written without thinking costs, and this +// one is meant to be usable in a place that has no divider in hardware. +inline constexpr kal_uintptr capacity = 4096; +static_assert((capacity & (capacity - 1)) == 0, "the wrap is a mask"); + +struct ring { + // ⚠️ THE INDICES ARE THE WORDS THAT ARE WAITED UPON, so they are the width + // kal_task_wait takes and not a machine word. A wait upon a word of another + // width is not a question the interface can be asked. + kal_u32 head; // written by the writer, read by the reader + kal_u32 tail; // written by the reader, read by the writer + kal_u32 closed; // set once by either end + unsigned char bytes[capacity]; +}; + +} // namespace kal::kit::detail + +export namespace kal::kit { + +// One end of a channel. Copying an end does not duplicate it: both copies name +// the same ring, and closing either closes it. The type is a handle, as +// everything in openkal is. +struct channel_end { + void* p; + bool is_reader; +}; + +struct channel_pair { + channel_end reader; + channel_end writer; + bool ok; +}; + +// Creates a channel. Both ends refer to one region; releasing is `close' on each +// end, and the region goes when the second one closes. +inline channel_pair channel_open() { + using detail::ring; + void* p = kal_alloc(sizeof(ring), alignof(ring)); + if (p == nullptr) return { {nullptr, true}, {nullptr, false}, false }; + + auto* r = static_cast(p); + r->head = 0; r->tail = 0; r->closed = 0; + return { {p, true}, {p, false}, true }; +} + +// How many bytes are held. Reported for a caller deciding whether to write, and +// not as a promise: another context may consume between the enquiry and the act. +inline kal_uintptr channel_pending(channel_end e) { + using detail::ring; + if (e.p == nullptr) return 0; + auto* r = static_cast(e.p); + return static_cast(r->head - r->tail); +} + +// Writes, sleeping while the ring is full. +// +// Reports what it wrote. A short write occurs only when the far end closed, and +// is distinguished from a complete one by the count rather than by an error, +// because a caller that wrote half its buffer needs to know how much. +inline kal_uintptr channel_write(channel_end e, const void* buf, kal_uintptr len) { + using detail::ring; + using detail::capacity; + if (e.p == nullptr || e.is_reader) return 0; + + auto* r = static_cast(e.p); + const auto* src = static_cast(buf); + kal_uintptr done = 0; + + while (done < len) { + if (r->closed) break; + + const kal_u32 head = r->head; + const kal_u32 tail = r->tail; + const kal_uintptr used = static_cast(head - tail); + if (used == capacity) { + // Full. Sleep upon the index the READER moves, so that the reader's + // wake reaches this context. Waiting upon our own index would be a + // wait nobody wakes. + kal_task_wait(&r->tail, tail, 0); + continue; + } + + const kal_uintptr room = capacity - used; + kal_uintptr n = len - done; + if (n > room) n = room; + for (kal_uintptr i = 0; i < n; ++i) + r->bytes[(head + i) & (capacity - 1)] = src[done + i]; + + r->head = head + static_cast(n); + done += n; + kal_uintptr woken = 0; + kal_task_wake(&r->head, 1, &woken); + } + return done; +} + +// Reads, sleeping while the ring is empty. +// +// Zero with the channel still open cannot occur: this returns only when it has +// bytes or when the far end has closed, which is what makes zero mean end of +// input as it does for kal_stream_read. +inline kal_uintptr channel_read(channel_end e, void* buf, kal_uintptr len) { + using detail::ring; + using detail::capacity; + if (e.p == nullptr || !e.is_reader || len == 0) return 0; + + auto* r = static_cast(e.p); + auto* dst = static_cast(buf); + + for (;;) { + const kal_u32 head = r->head; + const kal_u32 tail = r->tail; + const kal_uintptr used = static_cast(head - tail); + + if (used == 0) { + // ⚠️ THE CLOSE IS TESTED AFTER THE INDICES AND NOT BEFORE. A writer + // that filled the ring and closed in the same breath leaves bytes + // behind it; a reader that saw the close first would discard them + // and report an end of input that lost data. + if (r->closed) return 0; + kal_task_wait(&r->head, head, 0); + continue; + } + + kal_uintptr n = used < len ? used : len; + for (kal_uintptr i = 0; i < n; ++i) + dst[i] = r->bytes[(tail + i) & (capacity - 1)]; + + r->tail = tail + static_cast(n); + kal_uintptr woken = 0; + kal_task_wake(&r->tail, 1, &woken); + return n; + } +} + +// Closes one end. +// +// Both indices are woken, because a context may be asleep upon either: a writer +// waiting for room and a reader waiting for bytes must both learn that no more +// is coming. Waking only the one this end moves is the deadlock this arrangement +// invites. +inline void channel_close(channel_end e) { + using detail::ring; + if (e.p == nullptr) return; + auto* r = static_cast(e.p); + r->closed = 1; + kal_uintptr woken = 0; + kal_task_wake(&r->head, 64, &woken); + kal_task_wake(&r->tail, 64, &woken); +} + +// Releases the region. Called once, after both ends are closed; the pair is the +// unit of ownership and neither end owns it alone. +inline void channel_destroy(channel_pair& c) { + using detail::ring; + if (c.reader.p == nullptr) return; + kal_free(c.reader.p, sizeof(ring), alignof(ring)); + c.reader.p = nullptr; + c.writer.p = nullptr; + c.ok = false; +} + +} // namespace kal::kit diff --git a/kit/src/endpoint.cppm b/kit/src/endpoint.cppm new file mode 100644 index 0000000..91df6a1 --- /dev/null +++ b/kit/src/endpoint.cppm @@ -0,0 +1,144 @@ +// openkal.kit.endpoint --- writing an endpoint down, and reading one back. +// +// Clause 3.4 excludes name resolution from the specification in terms: an +// implementation shall not be required to parse an unbounded set of name +// schemes. That exclusion is about what an IMPLEMENTATION must provide, and it +// leaves every caller of openkal.net with the same small problem --- a +// configuration file says "127.0.0.1:8080" and kal_net_connect takes bytes and a +// number. +// +// Solving it once here is what this package is for. Nothing below reaches the +// network or the environment: this is arithmetic over a string, so it holds on a +// machine with firmware and nothing else, and it is not the resolution clause +// 3.4 declines. Turning a NAME into an address needs a resolver, which needs the +// network, and belongs above this module rather than in it. +export module openkal.kit.endpoint; + +import openkal.types; + +export namespace kal::kit { + +// What a parse produced, and whether it produced anything. +// +// A result type rather than an out-parameter and a code, because a caller that +// forgets to test an out-parameter reads a zeroed endpoint as the address +// 0.0.0.0 --- a legitimate address, and therefore a failure that looks like +// success. Here the endpoint is not reachable without the flag being visible. +struct endpoint_result { + kal_endpoint ep; + bool ok; +}; + +// Reads "A.B.C.D:port" or "A.B.C.D". +// +// THE ACCEPTED FORM IS EXACT AND THE REJECTIONS ARE NOT SILENT. A parser that +// accepted "1.2.3" as 1.2.0.3, or "300.1.1.1" as 44.1.1.1, would turn a +// mistyped configuration into a connection to somewhere else. Each component is +// required, each is bounded, and a component with a leading zero is refused +// because two conventions read "010" differently and neither of them is +// obviously the one the writer meant. +constexpr endpoint_result parse_v4(const char* text, kal_uintptr len) { + kal_endpoint ep{}; + ep.addr_len = 4; + + if (text == nullptr || len == 0) return { ep, false }; + + kal_uintptr i = 0; + for (int octet = 0; octet < 4; ++octet) { + if (octet > 0) { + if (i >= len || text[i] != '.') return { ep, false }; + ++i; + } + const kal_uintptr start = i; + unsigned value = 0; + while (i < len && text[i] >= '0' && text[i] <= '9') { + value = value * 10u + static_cast(text[i] - '0'); + if (value > 255u) return { ep, false }; + ++i; + if (i - start > 3) return { ep, false }; + } + if (i == start) return { ep, false }; // no digits + if (i - start > 1 && text[start] == '0') return { ep, false }; // leading zero + ep.addr[octet] = static_cast(value); + } + + if (i == len) { ep.port = 0; return { ep, true }; } // address alone + + if (text[i] != ':') return { ep, false }; + ++i; + const kal_uintptr pstart = i; + unsigned port = 0; + while (i < len && text[i] >= '0' && text[i] <= '9') { + port = port * 10u + static_cast(text[i] - '0'); + if (port > 65535u) return { ep, false }; + ++i; + } + if (i == pstart) return { ep, false }; // a colon and no port + if (i != len) return { ep, false }; // trailing rubbish + if (i - pstart > 1 && text[pstart] == '0') return { ep, false }; + + ep.port = static_cast(port); + return { ep, true }; +} + +// The inverse, which is why it is here rather than in a caller. +// +// Clause 7.11 states that an enquiry has an inverse, and the same reasoning +// applies to a conversion: a program that reads an endpoint from a +// configuration file and later reports which endpoint it used should not have +// to write the formatting itself, and two hand-written formatters in one +// program will eventually disagree. +// +// Writes at most 22 characters and reports how many. Nothing is allocated, +// because a facility usable on a machine with firmware and nothing else cannot +// assume an allocator is present even though openkal.memory is core. +struct format_result { + kal_uintptr n; + bool ok; +}; + +constexpr format_result format_v4(const kal_endpoint& ep, char* out, kal_uintptr cap) { + if (out == nullptr || ep.addr_len != 4) return { 0, false }; + + // "255.255.255.255:65535" is twenty-one characters. + if (cap < 22) return { 0, false }; + + kal_uintptr n = 0; + auto digits = [&](unsigned v) { + if (v >= 100) out[n++] = static_cast('0' + (v / 100) % 10); + if (v >= 10) out[n++] = static_cast('0' + (v / 10) % 10); + out[n++] = static_cast('0' + v % 10); + }; + + for (int octet = 0; octet < 4; ++octet) { + if (octet > 0) out[n++] = '.'; + digits(static_cast(ep.addr[octet])); + } + if (ep.port != 0) { + out[n++] = ':'; + digits(static_cast(ep.port)); + } + return { n, true }; +} + +// The loopback address, which every program that opens a listener for its own +// use writes out by hand otherwise. +constexpr kal_endpoint loopback_v4(kal_u32 port) { + kal_endpoint ep{}; + ep.addr[0] = 127; ep.addr[3] = 1; + ep.addr_len = 4; + ep.port = port; + return ep; +} + +// An endpoint that accepts on every address of the machine. Written out for the +// same reason: a program that means "listen everywhere" should say so rather +// than encode four zeroes whose meaning a reader must recall. +constexpr kal_endpoint any_v4(kal_u32 port) { + kal_endpoint ep{}; + ep.addr_len = 4; + ep.port = port; + return ep; +} + +} // namespace kal::kit diff --git a/kit/tests/kit.cpp b/kit/tests/kit.cpp new file mode 100644 index 0000000..c41b390 --- /dev/null +++ b/kit/tests/kit.cpp @@ -0,0 +1,155 @@ +// What this package composes, examined by using it. +// +// The observations are of behaviour. A test that named each entity and did +// nothing with it would compile against a package whose every operation did +// nothing, and would report that as working. +#include +#include +import openkal.types; +import openkal.task; +import openkal.kit.endpoint; +import openkal.kit.channel; + +namespace { + +int failures = 0; +void check(bool held, const char* what) { + if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +} + +void endpoint_section() { + using namespace kal::kit; + + // The ordinary forms. + { + const auto r = parse_v4("127.0.0.1:8080", 14); + check(r.ok, "an address with a port parses"); + check(r.ep.addr_len == 4, "the length says which kind of address it is"); + check(r.ep.addr[0] == 127 && r.ep.addr[1] == 0 && + r.ep.addr[2] == 0 && r.ep.addr[3] == 1, + "the octets are the octets, in order"); + check(r.ep.port == 8080, "the port is a number in host order"); + } + { + const auto r = parse_v4("10.0.0.255", 10); + check(r.ok && r.ep.port == 0, "an address without a port parses, with no port"); + check(r.ep.addr[3] == 255, "the largest octet is accepted"); + } + + // THE REJECTIONS ARE WHAT MAKES THIS WORTH HAVING. A parser that accepted + // these would turn a mistyped configuration into a connection to somewhere + // else, which is a defect that reports nothing at the time. + struct { const char* text; const char* why; } bad[] = { + { "1.2.3", "three components are not an address" }, + { "1.2.3.4.5", "five components are not an address" }, + { "300.1.1.1", "an octet above 255 is refused" }, + { "1.2.3.4:70000", "a port above 65535 is refused" }, + { "01.2.3.4", "a leading zero is refused rather than guessed at" }, + { "1.2.3.4:", "a colon with no port is refused" }, + { "1.2.3.4:80x", "trailing text is refused" }, + { "1..3.4", "an empty component is refused" }, + { "", "an empty string is refused" }, + }; + for (auto& b : bad) { + const auto r = parse_v4(b.text, std::strlen(b.text)); + check(!r.ok, b.why); + } + + // The inverse. Clause 7.11's reasoning applied to a conversion: a program + // that reads an endpoint and later reports it should not write the + // formatting itself. + { + char buf[32] = {}; + const auto ep = loopback_v4(443); + const auto f = format_v4(ep, buf, sizeof buf); + check(f.ok && f.n == 13 && std::memcmp(buf, "127.0.0.1:443", 13) == 0, + "an endpoint formats back to what it was written as"); + + // And round trips, which is the property the pair exists for. + const auto again = parse_v4(buf, f.n); + check(again.ok && again.ep.port == ep.port && + std::memcmp(again.ep.addr, ep.addr, 4) == 0, + "formatting and parsing are inverses"); + } + { + char small[8] = {}; + const auto f = format_v4(loopback_v4(443), small, sizeof small); + check(!f.ok, "a buffer too small is refused rather than half filled"); + } + { + const auto any = any_v4(80); + check(any.addr[0] == 0 && any.addr[3] == 0 && any.port == 80, + "the any-address is four zeroes and a port"); + } +} + +// The channel, exercised across two contexts, which is the only way to observe +// that a reader sleeps rather than spins and that a writer wakes it. +kal::kit::channel_end g_writer{}; +constexpr kal_uintptr kPayload = 100000; // far larger than the ring + +void producer(void*) { + unsigned char block[997]; // deliberately not a divisor of the ring + for (auto& b : block) b = 0xA5; + kal_uintptr sent = 0; + while (sent < kPayload) { + kal_uintptr n = kPayload - sent; + if (n > sizeof block) n = sizeof block; + const kal_uintptr put = kal::kit::channel_write(g_writer, block, n); + if (put == 0) break; + sent += put; + } + kal::kit::channel_close(g_writer); +} + +void channel_section() { + using namespace kal::kit; + + auto c = channel_open(); + check(c.ok, "a channel is created"); + if (!c.ok) return; + + g_writer = c.writer; + + kal_task t{}; + const int rc = kal_task_start(&producer, nullptr, &t); + check(rc == kal_ok, "a second context starts to write into it"); + if (rc != kal_ok) { channel_destroy(c); return; } + + // MORE THAN THE RING HOLDS, WHICH IS THE POINT. A payload that fitted would + // never make the writer wait, and the sleeping and waking this module exists + // for would go unobserved. + unsigned char buf[1024]; + kal_uintptr got = 0; + bool all_a5 = true; + for (;;) { + const kal_uintptr n = channel_read(c.reader, buf, sizeof buf); + if (n == 0) break; // the writer closed + for (kal_uintptr i = 0; i < n; ++i) + if (buf[i] != 0xA5) all_a5 = false; + got += n; + } + + kal_task_join(t); + check(got == kPayload, "every byte written is read"); + if (got != kPayload) + std::printf(" read %llu of %llu\n", + (unsigned long long)got, (unsigned long long)kPayload); + check(all_a5, "the bytes read are the bytes written"); + + // A read after the close reports end of input rather than waiting. + const kal_uintptr after = channel_read(c.reader, buf, sizeof buf); + check(after == 0, "a read after the far end closed reports end of input"); + + channel_close(c.reader); + channel_destroy(c); +} + +} // namespace + +int main() { + endpoint_section(); + channel_section(); + if (failures == 0) std::printf("openkal-kit: every observation held\n"); + return failures == 0 ? 0 : 1; +} diff --git a/mcpp.toml b/mcpp.toml index cbffd40..8e044c8 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.7.0" +version = "0.8.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/datagram.cppm b/src/datagram.cppm new file mode 100644 index 0000000..82c92e8 --- /dev/null +++ b/src/datagram.cppm @@ -0,0 +1,85 @@ +// openkal.datagram --- messages with boundaries, sent without a connection. +// +// A separate interface from openkal.net, for the reason clause 6.4 gives: a +// datagram and a connection are two resources, and an operation that some +// resources of an interface can never satisfy does not belong in it. A message +// boundary is not a property a byte stream has; ordering is not a property a +// datagram has. +// +// And it is the easier half to provide. A board that carries an IP stack +// reaches datagrams in a few hundred lines and connections in a few thousand, +// so an implementation supplying only this one is ordinary rather than +// deficient --- clause 6.1 expresses that by absence, and no capability word is +// needed to say it. +module; +#include + +export module openkal.datagram; +export import openkal.types; + +export using ::kal_datagram; + +export using ::kal_datagram_open; +export using ::kal_datagram_local; +export using ::kal_datagram_send_to; +export using ::kal_datagram_recv_from; +export using ::kal_datagram_close; +export using ::kal_datagram_props; + +static_assert(sizeof(kal_datagram) == sizeof(kal_uintptr), "clause 7.2"); + +export namespace kal::datagram { + +using datagram = kal_datagram; +using endpoint = kal_endpoint; + +struct props_tag; +using props = kal::props; + +inline constexpr props ipv6 {KAL_DGRAM_PROP_IPV6}; +inline constexpr props broadcast{KAL_DGRAM_PROP_BROADCAST}; + +inline props properties() { return props{kal_datagram_props}; } +inline bool has(props p) { return properties().has(p); } + +struct open_result { datagram d; int e; }; +struct endpoint_result { endpoint ep; int e; }; + +// A null local endpoint asks for one that may send and whose receiving address +// is unspecified. Two overloads rather than a defaulted pointer, so that the +// two intents are two calls at the call site. +inline open_result open(const endpoint& local) { + datagram d{}; + const int e = kal_datagram_open(&local, &d); + return { d, e }; +} +inline open_result open() { + datagram d{}; + const int e = kal_datagram_open(nullptr, &d); + return { d, e }; +} + +inline endpoint_result local(datagram d) { + endpoint ep{}; + const int e = kal_datagram_local(d, &ep); + return { ep, e }; +} + +inline kal_io_result send_to(datagram d, const void* p, kal_uintptr n, const endpoint& to) { + return kal_datagram_send_to(d, p, n, &to); +} + +// The sender is reported beside the result rather than through an +// out-parameter: every caller of a receive wants both, and none of them wants +// to declare an endpoint first. +struct recv_result { kal_io_result r; endpoint from; }; + +inline recv_result recv_from(datagram d, void* p, kal_uintptr n) { + endpoint from{}; + const kal_io_result r = kal_datagram_recv_from(d, p, n, &from); + return { r, from }; +} + +inline void close(datagram d) { kal_datagram_close(d); } + +} diff --git a/src/net.cppm b/src/net.cppm new file mode 100644 index 0000000..a598fd5 --- /dev/null +++ b/src/net.cppm @@ -0,0 +1,98 @@ +// openkal.net --- a connection, which is a stream with a peer and a way to be +// half-closed. +// +// Not merged with openkal.fs, and clause 3.4 records why: positioning applies +// to a file and not to a connection, half-closure to a connection and not to a +// file. A connection IS a stream once established, so this interface adds no +// transfer operation of its own --- kal_stream_read and kal_stream_write move +// its bytes. What is here is what a connection has and a stream in general +// does not. +// +// Name resolution is not here. Clause 3.4 excludes it in terms: an +// implementation shall not be required to parse an unbounded set of name +// schemes. +module; +#include + +export module openkal.net; +export import openkal.types; +export import openkal.stream; + +export using ::kal_net_conn; +export using ::kal_net_listener; + +export using ::kal_net_connect; +export using ::kal_net_listen; +export using ::kal_net_accept; +export using ::kal_net_stream; +export using ::kal_net_peer; +export using ::kal_net_local; +export using ::kal_net_listener_local; +export using ::kal_net_shutdown; +export using ::kal_net_close; +export using ::kal_net_close_listener; +export using ::kal_net_props; + +static_assert(sizeof(kal_net_conn) == sizeof(kal_uintptr), "clause 7.2"); +static_assert(sizeof(kal_net_listener) == sizeof(kal_uintptr), "clause 7.2"); + +export namespace kal::net { + +using conn = kal_net_conn; +using listener = kal_net_listener; +using endpoint = kal_endpoint; + +struct props_tag; +using props = kal::props; + +inline constexpr props ipv6 {KAL_NET_PROP_IPV6}; +inline constexpr props halfclose{KAL_NET_PROP_HALFCLOSE}; + +inline props properties() { return props{kal_net_props}; } +inline bool has(props p) { return properties().has(p); } + +// The directions kal_net_shutdown accepts. An enumeration rather than the bare +// integers, so that a caller cannot pass a stream flag by mistake. +enum class shut { read = KAL_SHUT_READ, write = KAL_SHUT_WRITE, both = KAL_SHUT_BOTH }; + +inline int shutdown(conn c, shut d) { + return kal_net_shutdown(c, static_cast(d)); +} + +// A handle and the outcome together, so that the ordinary use --- open it, test +// it, use it --- is one statement. The handle is meaningful only when e is +// kal_ok; nothing here enforces that, for the same reason the C form does not. +struct conn_result { conn c; int e; }; +struct listener_result { listener l; int e; }; +struct endpoint_result { endpoint ep; int e; }; + +inline conn_result connect(const endpoint& to) { + conn c{}; + const int e = kal_net_connect(&to, &c); + return { c, e }; +} + +inline listener_result listen(const endpoint& local) { + listener l{}; + const int e = kal_net_listen(&local, &l); + return { l, e }; +} + +inline conn_result accept(listener l) { + conn c{}; + const int e = kal_net_accept(l, &c); + return { c, e }; +} + +// The stream a connection owns. Named `stream' rather than converted +// implicitly, so that a caller can see where the borrowing begins. +inline kal_stream stream(conn c) { return kal_stream{kal_net_stream(c)}; } + +inline endpoint_result peer (conn c) { endpoint ep{}; const int e = kal_net_peer (c, &ep); return { ep, e }; } +inline endpoint_result local(conn c) { endpoint ep{}; const int e = kal_net_local(c, &ep); return { ep, e }; } +inline endpoint_result local(listener l) { endpoint ep{}; const int e = kal_net_listener_local(l, &ep); return { ep, e }; } + +inline void close(conn c) { kal_net_close(c); } +inline void close(listener l) { kal_net_close_listener(l); } + +} diff --git a/src/process.cppm b/src/process.cppm index b86b2a9..9af18f0 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -16,11 +16,18 @@ module; export module openkal.process; export import openkal.types; export import openkal.fs; +// For kal_process_channel, whose pair of streams is what a parent speaks to a +// started program through. +export import openkal.stream; export using ::kal_process; export using ::kal_spawn_streams; +export using ::kal_preopen; export using ::kal_process_spawn; +export using ::kal_process_spawn_with; +export using ::kal_process_channel; +export using ::kal_process_channel_close; export using ::kal_process_wait; export using ::kal_process_terminate; export using ::kal_process_close; @@ -28,6 +35,7 @@ export using ::kal_process_props; static_assert(sizeof(kal_process) == sizeof(kal_uintptr), "clause 7.2"); static_assert(sizeof(kal_spawn_streams) == 3 * sizeof(kal_uintptr), "clause 5.3"); +static_assert(sizeof(kal_preopen) == 3 * sizeof(kal_uintptr), "clause 5.3"); export namespace kal::process { @@ -40,8 +48,25 @@ using props = kal::props; inline constexpr props terminate {KAL_PROCESS_PROP_TERMINATE}; 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 props properties() { return props{kal_process_props}; } inline bool has(props p) { return properties().has(p); } +using preopen = kal_preopen; + +// Both ends of a channel, returned together. A parent that does not release +// `theirs' after the spawn never observes the end of input on `mine', so the +// two are handed over as one value to make the pairing hard to overlook. +struct channel_result { kal_stream mine; kal_stream theirs; int e; }; + +inline channel_result channel_open() { + kal_stream mine{}, theirs{}; + const int e = kal_process_channel(&mine, &theirs); + return { mine, theirs, e }; +} + +inline void channel_close(kal_stream s) { kal_process_channel_close(s); } + } diff --git a/src/space.cppm b/src/space.cppm new file mode 100644 index 0000000..09d7faf --- /dev/null +++ b/src/space.cppm @@ -0,0 +1,51 @@ +// openkal.space --- an address space, and a context executing in one. +// +// This is not fork. Clause 7.1 refuses to require the duplication of an address +// space AND its execution state; what is here is the first half alone. A +// context started in a cloned space begins at a function the caller names, not +// at the instruction the caller was executing --- which is what lets this be +// stated in a C application binary interface at all. +// +// A library above this interface reaches fork by saving its own execution state +// before the call and restoring it in the started context. That is composition, +// and it belongs above this line rather than in it: the saving is done with the +// compiler's own facilities, differs per architecture, and is not something a +// kernel interface can perform on a caller's behalf. +// +// One operation and not two. An earlier form separated the copying of the space +// from the starting of a context in it, so that a caller held a space as a +// handle. No environment this specification targets has that pair as a +// primitive, and an implementation asked to separate them would have to park a +// started context and build a channel by which to tell it what to run --- a +// mechanism reconstructed, which clause 7.1 identifies as a fault in the shape +// of the specification rather than in the implementation. +module; +#include + +export module openkal.space; +export import openkal.types; +export import openkal.process; + +export using ::kal_space_start; +export using ::kal_space_props; + +export namespace kal::space { + +struct props_tag; +using props = kal::props; + +inline constexpr props clone_handles{KAL_SPACE_PROP_CLONE_HANDLES}; +inline constexpr props deferred_copy{KAL_SPACE_PROP_DEFERRED_COPY}; + +inline props properties() { return props{kal_space_props}; } +inline bool has(props p) { return properties().has(p); } + +struct process_result { kal_process p; int e; }; + +inline process_result start(void (*entry)(void*), void* arg, void* stack_top) { + kal_process p{}; + const int e = kal_space_start(entry, arg, stack_top, &p); + return { p, e }; +} + +} diff --git a/src/terminal.cppm b/src/terminal.cppm new file mode 100644 index 0000000..9cbd6ca --- /dev/null +++ b/src/terminal.cppm @@ -0,0 +1,68 @@ +// openkal.terminal --- what an interactive stream does with what is typed at +// it. +// +// A separate interface rather than operations upon openkal.stream, for the +// reason clause 6.4 gives when it places positioning in openkal.fs: the +// behaviour varies between the RESOURCES of the stream interface rather than +// between implementations. The same implementation answers one way for a +// terminal and another for a file, so an implementation could neither claim +// these operations honestly nor withhold them usefully. +// +// The pair is get/set and not two setters. A program that turns line editing +// off must be able to put back what was there, and a setter alone would let it +// restore only a default --- the terminal a user returns to would then not be +// the one they had. Clause 7.11 states the rule; this is an instance of it. +module; +#include + +export module openkal.terminal; +export import openkal.types; +export import openkal.stream; + +export using ::kal_terminal_get_mode; +export using ::kal_terminal_set_mode; +export using ::kal_terminal_size; +export using ::kal_terminal_props; + +export namespace kal::terminal { + +// Positions in the MODE word. Distinct from the props word below: one says +// what the terminal is doing, the other what this implementation can be asked. +// They are separate types so that a program cannot test one against the other. +struct mode_tag; +using mode = kal::props; + +inline constexpr mode line_edit{KAL_TERM_LINE_EDIT}; +inline constexpr mode echo {KAL_TERM_ECHO}; + +struct props_tag; +using props = kal::props; + +inline constexpr props has_mode{KAL_TERM_PROP_MODE}; +inline constexpr props has_size{KAL_TERM_PROP_SIZE}; + +inline props properties(kal_stream s) { return props{kal_terminal_props(s)}; } +inline bool has(kal_stream s, props p) { return properties(s).has(p); } + +// The mode currently in effect. Reported alongside the error rather than +// through an out-parameter, so that a caller which restores what it found +// writes one statement and not three. +struct mode_result { mode m; int e; }; + +inline mode_result get_mode(kal_stream s) { + kal_uintptr bits = 0; + const int e = kal_terminal_get_mode(s, &bits); + return { mode{bits}, e }; +} + +inline int set_mode(kal_stream s, mode m) { return kal_terminal_set_mode(s, m.bits); } + +struct size_result { kal_uintptr cols; kal_uintptr rows; int e; }; + +inline size_result size(kal_stream s) { + kal_uintptr c = 0, r = 0; + const int e = kal_terminal_size(s, &c, &r); + return { c, r, e }; +} + +} diff --git a/src/timeout.cppm b/src/timeout.cppm new file mode 100644 index 0000000..8ab82f1 --- /dev/null +++ b/src/timeout.cppm @@ -0,0 +1,75 @@ +// openkal.timeout --- a bound upon operations that would otherwise wait +// without end. +// +// Every operation here is the operation of the same name in another interface, +// with one argument added. Clause 7.8 already establishes that a second form of +// one operation is admissible when the first cannot state the whole of an +// intent: kal_fs_open_file and kal_fs_open stand beside each other for that +// reason, and these stand beside their originals for the same one. +// +// The argument is a duration, not an instant, and the name of this interface +// says so. kal_task_wait already takes a timeout_ns and already defines zero as +// no timeout; a second spelling of the same idea would be the one thing this +// specification most consistently refuses. +// +// An expired bound is reported as kal_err_again --- "the operation would +// block" --- which is what an expiry is. The error set is closed (clause 5.2) +// and required no addition. +module; +#include + +export module openkal.timeout; +export import openkal.types; +export import openkal.stream; +export import openkal.net; +export import openkal.datagram; +export import openkal.process; + +export using ::kal_timeout_read; +export using ::kal_timeout_write; +export using ::kal_timeout_accept; +export using ::kal_timeout_recv_from; +export using ::kal_timeout_wait_process; +export using ::kal_timeout_granularity_ns; + +export namespace kal::timeout { + +// The smallest bound this implementation distinguishes. A caller that asks for +// less is not refused and does not get less. +inline kal_uintptr granularity_ns() { return kal_timeout_granularity_ns; } + +inline kal_io_result read (kal_stream s, void* p, kal_uintptr n, kal_u64 ns) { + return kal_timeout_read(s, p, n, ns); +} +inline kal_io_result write(kal_stream s, const void* p, kal_uintptr n, kal_u64 ns) { + return kal_timeout_write(s, p, n, ns); +} + +struct conn_result { kal_net_conn c; int e; }; + +inline conn_result accept(kal_net_listener l, kal_u64 ns) { + kal_net_conn c{}; + const int e = kal_timeout_accept(l, ns, &c); + return { c, e }; +} + +// Shaped like kal::datagram::recv_from, because it is that operation with a +// bound: a caller that adds a timeout should not also have to change how it +// reads the result. +struct recv_result { kal_io_result r; kal_endpoint from; }; + +inline recv_result recv_from(kal_datagram d, void* p, kal_uintptr n, kal_u64 ns) { + kal_endpoint from{}; + const kal_io_result r = kal_timeout_recv_from(d, p, n, &from, ns); + return { r, from }; +} + +struct wait_result { int status; int terminated; int e; }; + +inline wait_result wait(kal_process p, kal_u64 ns) { + int status = 0, terminated = 0; + const int e = kal_timeout_wait_process(p, ns, &status, &terminated); + return { status, terminated, e }; +} + +} diff --git a/src/types.cppm b/src/types.cppm index 87946bb..3c02890 100644 --- a/src/types.cppm +++ b/src/types.cppm @@ -19,6 +19,7 @@ export using ::kal_uintptr; export using ::kal_u32; export using ::kal_u64; export using ::kal_i64; +export using ::kal_u8; // The complete set of error conditions openkal defines. The set is closed: an // implementation maps its environment's error values onto these and does not @@ -41,6 +42,15 @@ export using ::kal_err_not_directory; // The result of an operation that transfers a count. export using ::kal_io_result; +export using ::kal_endpoint; + +// Clause 5.3 freezes the layout. The address is twenty-four bytes so that one +// carrying a scope identifier fits without a second type; a build in which it +// were not would read every address at the wrong offset while still linking. +static_assert(sizeof(kal_endpoint{}.addr) == 24, + "clause 5.3: an endpoint address is twenty-four bytes"); +static_assert(sizeof(kal_endpoint) >= 24 + sizeof(kal_uintptr) + sizeof(kal_u32), + "clause 5.3: an endpoint holds its address, a length and a port"); // Clause 5.3 declares the layout of every structure immutable. A declaration // that something shall not change is not a mechanism; this is the mechanism. diff --git a/tools/run-kit-tests.sh b/tools/run-kit-tests.sh new file mode 100755 index 0000000..a95f133 --- /dev/null +++ b/tools/run-kit-tests.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Runs openkal-kit's tests against a working tree of an implementation. +# +# run-kit-tests.sh +# +# The kit's manifest names the specification by a path --- it is in the same +# repository --- and an implementation by version, which is what a published +# manifest must say. Neither form is what a run spanning two branches needs, so +# both are rewritten here to name the working trees, exactly as +# run-conformance.sh does and for the same reason: a change spanning the +# specification and an implementation must be tested against both halves as +# written rather than against whichever half is published. +# +# THE REWRITE MUST NOT SURVIVE THE RUN. run-conformance.sh records that leaving +# one behind put a local absolute path into a public repository; the trap below +# is why that cannot happen here. +set -euo pipefail + +package="${1:?usage: run-kit-tests.sh }" +implementation="${2:?usage: run-kit-tests.sh }" +# Consumed, so that "$@" below carries only what the caller meant for the build +# tool. Without this the two arguments above are handed to `mcpp test` as test +# filters, no test matches, and the run reports success having examined nothing. +shift 2 + +here="$(cd "$(dirname "$0")/.." && pwd)" +impl="$(cd "$implementation" && pwd)" + +native() { # a path the build tool understands, on every system it runs on + if command -v cygpath > /dev/null 2>&1; then cygpath -m "$1"; else printf '%s\n' "$1"; fi +} +here_native="$(native "$here")" +impl_native="$(native "$impl")" + +kit="$here/kit" +manifests=("$kit/mcpp.toml" "$impl/mcpp.toml") + +restore() { + for m in "${manifests[@]}"; do + [ -f "$m.orig" ] || continue + mv -f "$m.orig" "$m" + done +} +trap restore EXIT + +for m in "${manifests[@]}"; do cp "$m" "$m.orig"; done + +# The implementation reaches the specification by whatever its own manifest says +# --- a released version, or a branch. Both are replaced by this working tree, so +# that what is tested is what is written here. +sed -i.bak -E "s|^openkal = .*$|openkal = { path = \"$here_native\" }|" "$impl/mcpp.toml" +rm -f "$impl/mcpp.toml.bak" + +# And the kit reaches the implementation by version; that becomes this tree. +sed -i.bak -E "s|^${package} = \{ version = \"[^\"]*\"(.*)$|${package} = { path = \"$impl_native\"\1|" \ + "$kit/mcpp.toml" +rm -f "$kit/mcpp.toml.bak" + +# ⚠️ ASSERTED RATHER THAN ASSUMED. A substitution that matched nothing leaves the +# manifest naming a version, the resolver fetches a published implementation, and +# the run reports on that one while appearing to report on this branch. +grep -q "path = \"$impl_native\"" "$kit/mcpp.toml" \ + || { echo "the implementation substitution matched nothing in kit/mcpp.toml" >&2; exit 2; } +grep -q "path = \"$here_native\"" "$impl/mcpp.toml" \ + || { echo "the specification substitution matched nothing in $impl/mcpp.toml" >&2; exit 2; } + +echo "--- the kit's dependencies ---" +sed -n '/^\[dependencies\]/,/^$/p;/dev-dependencies/,+2p' "$kit/mcpp.toml" +echo + +( cd "$kit" && mcpp test "$@" )