From a806c0a2d55405de75e9381310ca9f4250273a1b Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 13:05:33 +0800 Subject: [PATCH 1/9] spec: version 0.8 adds five optional interfaces The five are terminal control, connections, datagrams, address spaces, and a bound upon operations that would otherwise wait without end. Each is optional in the sense clause 3 defines: an environment with no network is not deficient for providing neither openkal.net nor openkal.datagram, and one 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 requiring one is refused when it is built rather than when it runs. The admission criterion applied to each was the same, and three candidates did not meet it. An interface enters the specification only when it is a minimal capability that every kernel has, cannot be composed from the atoms already specified, and is usable in both directions --- composable from above and implementable from below. What can be composed belongs in a library above the line. ## Decomposition openkal.terminal is 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 pair is get/set and not two setters, because a program that turns line editing off must be able to restore what was there rather than a default. openkal.net and openkal.datagram are separate from each other on the same grounds. A message boundary is not a property a byte stream has; ordering is not a property a datagram has. Datagrams are also the easier half to provide, so an implementation supplying only that one is ordinary rather than deficient. openkal.space is not fork. Clause 7.1 refuses to require the duplication of an address space AND its execution state; what is specified is the first half alone. A library above the interface reaches fork by saving its own execution state before the clone and restoring it in the new context, which is composition performed with the compiler's own facilities. openkal.timeout states a duration rather than an instant, because kal_task_wait already does and a second spelling of one idea is what this specification most consistently refuses. An expired bound is kal_err_again; the closed error set of clause 5.2 required no addition. ## Types struct kal_endpoint carries bytes and a length rather than a tagged union of families. The length distinguishes one kind of address from another and is a VALUE rather than a layout, so the set of lengths defined may grow while clause 5.3 continues to hold the structure fixed. An implementation refuses a length it does not know rather than reading it as one it does. It is declared in types.h because both openkal.net and openkal.datagram use it and either may be provided without the other. ## Additions to openkal.process kal_process_spawn_with starts a program receiving exactly the directories named, and is the inverse of kal_fs_preopen under clause 7.11. kal_process_channel yields a pair of streams of which one end crosses a spawn boundary; both ends are owned, and the release is declared beside them because a parent that does not release the far end never observes the end of input on its own. Both are second declarations rather than arguments added to existing ones, because clause 8 forbids altering one that exists. ## Verification declarations 91 names, compiled without the environment's headers types 150 declarations, no backend type named modules the five new interfaces compile The two entries clause 6.3 gains record readiness notification and an instant rather than a duration, with the property of implementations that excluded each. Clause 11 gains six entries. The entry on duplication of the calling image states that fork is refused as an OPERATION and that the atoms from which a library may compose it are now specified, so that a later reader does not conclude the capability was excluded along with the operation. --- SPEC.md | 76 ++++++++++++- SURFACE.txt | 40 ++++++- conformance/mcpp.toml | 16 ++- conformance/src/declarations.c | 33 ++++++ conformance/src/sections/datagram.cpp | 114 ++++++++++++++++++++ conformance/src/sections/datagram.cppm | 6 ++ conformance/src/sections/net.cpp | 143 +++++++++++++++++++++++++ conformance/src/sections/net.cppm | 6 ++ conformance/src/sections/space.cpp | 109 +++++++++++++++++++ conformance/src/sections/space.cppm | 6 ++ conformance/src/sections/terminal.cpp | 123 +++++++++++++++++++++ conformance/src/sections/terminal.cppm | 6 ++ conformance/src/sections/timeout.cpp | 70 ++++++++++++ conformance/src/sections/timeout.cppm | 6 ++ conformance/src/spec.cppm | 38 ++++++- conformance/src/suite.cpp | 10 ++ include/openkal.h | 7 ++ include/openkal/datagram.h | 74 +++++++++++++ include/openkal/net.h | 84 +++++++++++++++ include/openkal/process.h | 48 +++++++++ include/openkal/space.h | 79 ++++++++++++++ include/openkal/terminal.h | 74 +++++++++++++ include/openkal/timeout.h | 63 +++++++++++ include/openkal/types.h | 40 +++++++ mcpp.toml | 2 +- src/datagram.cppm | 85 +++++++++++++++ src/net.cppm | 90 ++++++++++++++++ src/process.cppm | 25 +++++ src/space.cppm | 60 +++++++++++ src/terminal.cppm | 68 ++++++++++++ src/timeout.cppm | 75 +++++++++++++ 31 files changed, 1667 insertions(+), 9 deletions(-) create mode 100644 conformance/src/sections/datagram.cpp create mode 100644 conformance/src/sections/datagram.cppm create mode 100644 conformance/src/sections/net.cpp create mode 100644 conformance/src/sections/net.cppm create mode 100644 conformance/src/sections/space.cpp create mode 100644 conformance/src/sections/space.cppm create mode 100644 conformance/src/sections/terminal.cpp create mode 100644 conformance/src/sections/terminal.cppm create mode 100644 conformance/src/sections/timeout.cpp create mode 100644 conformance/src/sections/timeout.cppm create mode 100644 include/openkal/datagram.h create mode 100644 include/openkal/net.h create mode 100644 include/openkal/space.h create mode 100644 include/openkal/terminal.h create mode 100644 include/openkal/timeout.h create mode 100644 src/datagram.cppm create mode 100644 src/net.cppm create mode 100644 src/space.cppm create mode 100644 src/terminal.cppm create mode 100644 src/timeout.cppm diff --git a/SPEC.md b/SPEC.md index cae855b..9e7c93c 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 fifty. Clause 9's procedure examines this. @@ -413,6 +428,31 @@ 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. + +**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 +827,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..16574ed 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,38 @@ 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 +# 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_clone +kal_space_destroy +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..f6428b9 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,30 @@ 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_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_clone); + (void)sizeof(&kal_space_destroy); + (void)sizeof(&kal_space_props); + (void)sizeof(&kal_space_start); (void)sizeof(&kal_stderr); (void)sizeof(&kal_stdin); (void)sizeof(&kal_stdout); @@ -80,8 +103,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..defa8dd --- /dev/null +++ b/conformance/src/sections/net.cpp @@ -0,0 +1,143 @@ +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_stream 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_stream 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; } + + { + const char msg[] = "openkal"; + const kal_io_result w = kal_stream_write(client, 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(server, 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_props & KAL_NET_PROP_HALFCLOSE) != 0; + const int rc = kal_net_shutdown(client, KAL_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(server, 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_stream s{}; + const int rc = kal_net_connect(&odd, &s); + observe(kind::behaviour, rc == kal_err_invalid, + "an endpoint of unknown length is refused, not misread"); + if (rc == kal_ok) kal_net_close(s); + } +#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/space.cpp b/conformance/src/sections/space.cpp new file mode 100644 index 0000000..5f71d0b --- /dev/null +++ b/conformance/src/sections/space.cpp @@ -0,0 +1,109 @@ +module okc.space; + +import openkal.types; +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 the cloned space, +// not that it computed anything. A larger entry would make a failure ambiguous +// between "the clone did not work" and "the entry was wrong". +void entry(void* arg) { + if (arg != nullptr) *static_cast(arg) = 1; + kal_exit(0); +} + +// 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 OBSERVED BEFORE THE OPERATIONS, because what it says + // determines what a caller above this interface may do. An environment whose + // clone 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_props & KAL_SPACE_PROP_CLONE_HANDLES) != 0; + const bool deferred = (kal_space_props & KAL_SPACE_PROP_DEFERRED_COPY) != 0; + line(handles ? " a clone carries the handles the original holds" + : " a clone carries memory only, not handles"); + line(deferred ? " the copy may be completed lazily" + : " the copy is complete when the clone reports success"); + } + + kal_space sp{}; + const int crc = kal_space_clone(&sp); + if (crc != kal_ok) { + unobserved(kind::behaviour, "openkal.space", + "the address space could not be cloned in this environment"); + return; + } + observe(kind::behaviour, true, "the calling address space is cloned"); + + // A CONTEXT STARTED IN THE CLONE IS A PROCESS, which is why kal_process_wait + // applies to it unchanged rather than being restated in this interface. That + // it is waitable is the observation; what it computed is not, because the + // clone is a separate space and the caller cannot see its stores. + { + volatile int flag = 0; + kal_process p{}; + const int src = kal_space_start(sp, &entry, (void*)&flag, + child_stack + sizeof child_stack, &p); + observe(kind::behaviour, src == kal_ok, + "a context starts in the cloned space"); + if (src == kal_ok) { + 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"); + kal_process_close(p); + } + + // THE STORE IS NOT VISIBLE HERE, AND THAT IS THE POINT. A clone is a + // separate address space; a caller observing its own flag set would be + // observing a shared space, which is openkal.task and not this + // interface. + observe(kind::behaviour, flag == 0, + "a store in the cloned space is not observed in the original"); + } + + kal_space_destroy(sp); + + // The two handles are independent and either may be closed first. Destroying + // a space whose context has already ended is the ordinary order and is + // observed by the absence of a failure in the run that follows. + { + kal_space second{}; + const int rc = kal_space_clone(&second); + if (rc == kal_ok) { + kal_space_destroy(second); + observe(kind::behaviour, true, + "a cloned space with no context started in it is released"); + } else { + unobserved(kind::behaviour, "a space with no context is released", + "a second clone could not be made"); + } + } +#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..2b6d4f4 --- /dev/null +++ b/conformance/src/sections/terminal.cpp @@ -0,0 +1,123 @@ +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. + const kal_stream out = kal_stdout(); + const bool interactive = + (kal_stream_props(out) & KAL_STREAM_PROP_INTERACTIVE) != 0; + + 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 kal_uintptr props = kal_terminal_props(out); + if ((props & KAL_TERM_PROP_SIZE) != 0) { + 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 & KAL_TERM_PROP_MODE) != 0) { + 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/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..226f6c3 --- /dev/null +++ b/include/openkal/net.h @@ -0,0 +1,84 @@ +/* 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" + +/* An opaque handle occupying one machine word, per clause 7.2. A listener is + * not a stream: nothing is transferred through it, and giving it the stream + * operations would be an interface whose resource can never satisfy them. */ +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_stream* 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_stream* out); + +/* 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_stream s, struct kal_endpoint* out); +int kal_net_local (struct kal_stream s, 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_stream s, int direction); + +/* AN OWNED STREAM, UNLIKE THE THREE `openkal.stream' PROVIDES. A connection is + * obtained and must be released; the standard streams are borrowed and are not. + * This is the same division `openkal.fs' already draws with + * `kal_fs_close_file', and the reason the release lives here rather than in + * `openkal.stream': a stream in general has no owner to return it to. */ +void kal_net_close (struct kal_stream s); +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..37a666e --- /dev/null +++ b/include/openkal/space.h @@ -0,0 +1,79 @@ +/* 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 clone and restoring it in the new 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. + * + * THERE IS NO `create'. 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" + +/* An opaque handle occupying one machine word, per clause 7.2. */ +struct kal_space { kal_uintptr h; }; + +/* Positions in kal_space_props. */ + +/* Whether a clone carries the handles the original holds. + * + * An environment whose cloning primitive copies 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 cloned memory + * may fail after the clone 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 it 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 + +/* Clones the CALLER's address space. */ +int kal_space_clone(struct kal_space* out); + +/* Starts a context in the given 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 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. */ +int kal_space_start(struct kal_space sp, + void (*entry)(void*), void* arg, void* stack_top, + struct kal_process* out); + +/* Releases the space. + * + * A space whose context is running is not released until that process ends. The + * two handles are independent and either may be closed first, which is the same + * ownership rule clause 6.7 states generally. */ +void kal_space_destroy(struct kal_space sp); + +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..9488c53 --- /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_stream* 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/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..5f58eab --- /dev/null +++ b/src/net.cppm @@ -0,0 +1,90 @@ +// 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_listener; + +export using ::kal_net_connect; +export using ::kal_net_listen; +export using ::kal_net_accept; +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_listener) == sizeof(kal_uintptr), "clause 7.2"); + +export namespace kal::net { + +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(kal_stream s, shut d) { + return kal_net_shutdown(s, 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 stream_result { kal_stream s; int e; }; +struct listener_result { listener l; int e; }; +struct endpoint_result { endpoint ep; int e; }; + +inline stream_result connect(const endpoint& to) { + kal_stream s{}; + const int e = kal_net_connect(&to, &s); + return { s, e }; +} + +inline listener_result listen(const endpoint& local) { + listener l{}; + const int e = kal_net_listen(&local, &l); + return { l, e }; +} + +inline stream_result accept(listener l) { + kal_stream s{}; + const int e = kal_net_accept(l, &s); + return { s, e }; +} + +inline endpoint_result peer (kal_stream s) { endpoint ep{}; const int e = kal_net_peer (s, &ep); return { ep, e }; } +inline endpoint_result local(kal_stream s) { endpoint ep{}; const int e = kal_net_local(s, &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(kal_stream s) { kal_net_close(s); } +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..a126fe4 --- /dev/null +++ b/src/space.cppm @@ -0,0 +1,60 @@ +// 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 clone and restoring it in the new 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. +module; +#include + +export module openkal.space; +export import openkal.types; +export import openkal.process; + +export using ::kal_space; + +export using ::kal_space_clone; +export using ::kal_space_start; +export using ::kal_space_destroy; +export using ::kal_space_props; + +static_assert(sizeof(kal_space) == sizeof(kal_uintptr), "clause 7.2"); + +export namespace kal::space { + +using space = 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 clone_result { space sp; int e; }; +struct process_result { kal_process p; int e; }; + +inline clone_result clone() { + space sp{}; + const int e = kal_space_clone(&sp); + return { sp, e }; +} + +inline process_result start(space sp, void (*entry)(void*), void* arg, void* stack_top) { + kal_process p{}; + const int e = kal_space_start(sp, entry, arg, stack_top, &p); + return { p, e }; +} + +inline void destroy(space sp) { kal_space_destroy(sp); } + +} 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..86a378d --- /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 stream_result { kal_stream s; int e; }; + +inline stream_result accept(kal_net_listener l, kal_u64 ns) { + kal_stream s{}; + const int e = kal_timeout_accept(l, ns, &s); + return { s, 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 }; +} + +} From 8ac8a9a54c37b04a923c702c826c524fe8fb451a Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 13:14:49 +0800 Subject: [PATCH 2/9] spec: openkal.space is one operation, not two Withdrawn while the first implementation was being written, which is the point at which the defect became visible. The interface separated the copying of an address space from the starting of a context in it, so that a caller held a space as a handle and started a context in it afterwards. No environment this specification targets has that pair as a primitive. On Linux the copy and the start are one act: clone(2) copies the address space and begins execution in the copy, and there is no form that does the first without the second. An implementation asked to provide the separated form would therefore have to start a context anyway, park it upon a waiting primitive, and build a channel by which to tell it what to run afterwards. Clause 7.1 states the test and the verdict: an implementation that must maintain a registry in order to satisfy the specification indicates that the specification has taken a shape borrowed from one environment, and the shape is at fault rather than the implementation. The separated form also had an unstated question it could not answer. If a caller copies a space, modifies its own memory, and then starts a context, the copy the context sees is the one taken at the first call under one reading and at the second under the other. The single operation does not have the question. removed struct kal_space, kal_space_clone, kal_space_destroy retained kal_space_start, now taking the entry directly, and kal_space_props Clause 6.3 records the withdrawn form among the mechanisms considered and not adopted, with the property of implementations that excluded it, so that a later reader proposing it again finds the measurement rather than repeating it. The conformance section was rewritten around the exit status. A started context has an address space of its own, so a store it performs is not visible to the caller; how it ended is the one thing it can report, and the section therefore observes that the entry received its argument by having the entry choose its exit code from it. declarations 89 names, compiled without the environment's headers types 146 declarations, no backend type named --- SPEC.md | 15 +++- SURFACE.txt | 2 - conformance/src/declarations.c | 2 - conformance/src/sections/space.cpp | 114 +++++++++++++++-------------- include/openkal/space.h | 73 +++++++++--------- src/space.cppm | 33 +++------ 6 files changed, 127 insertions(+), 112 deletions(-) diff --git a/SPEC.md b/SPEC.md index 9e7c93c..30ea755 100644 --- a/SPEC.md +++ b/SPEC.md @@ -354,7 +354,7 @@ version 0.6 already satisfies it — one hundred and one of them, examined by th 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. 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 fifty. +it also; the count is now one hundred and forty-six. Clause 9's procedure examines this. @@ -445,6 +445,19 @@ 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 diff --git a/SURFACE.txt b/SURFACE.txt index 16574ed..6817519 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -102,8 +102,6 @@ kal_datagram_props kal_datagram_recv_from kal_datagram_send_to # openkal.space -kal_space_clone -kal_space_destroy kal_space_props kal_space_start # openkal.timeout diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index f6428b9..42ade8c 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -85,8 +85,6 @@ void okc_declarations_c(void) (void)sizeof(&kal_process_wait); (void)sizeof(&kal_random_fill); (void)sizeof(&kal_random_props); - (void)sizeof(&kal_space_clone); - (void)sizeof(&kal_space_destroy); (void)sizeof(&kal_space_props); (void)sizeof(&kal_space_start); (void)sizeof(&kal_stderr); diff --git a/conformance/src/sections/space.cpp b/conformance/src/sections/space.cpp index 5f71d0b..8dd9803 100644 --- a/conformance/src/sections/space.cpp +++ b/conformance/src/sections/space.cpp @@ -1,6 +1,7 @@ module okc.space; import openkal.types; +import openkal.abort; import openkal.process; import openkal.space; import okc.report; @@ -12,12 +13,16 @@ namespace okc::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 the cloned space, -// not that it computed anything. A larger entry would make a failure ambiguous -// between "the clone did not work" and "the entry was wrong". +// 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) { - if (arg != nullptr) *static_cast(arg) = 1; - kal_exit(0); + 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 @@ -35,72 +40,75 @@ void run() { #else claim("kal_space_props", kal_space_props); - // THE PROPERTY WORD IS OBSERVED BEFORE THE OPERATIONS, because what it says + // THE PROPERTY WORD IS REPORTED BEFORE THE OPERATION, because what it says // determines what a caller above this interface may do. An environment whose - // clone does not carry handles cannot support a library reaching for POSIX + // 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_props & KAL_SPACE_PROP_CLONE_HANDLES) != 0; const bool deferred = (kal_space_props & KAL_SPACE_PROP_DEFERRED_COPY) != 0; - line(handles ? " a clone carries the handles the original holds" - : " a clone carries memory only, not handles"); + 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 clone reports success"); + : " the copy is complete when the call reports success"); } - kal_space sp{}; - const int crc = kal_space_clone(&sp); - if (crc != kal_ok) { - unobserved(kind::behaviour, "openkal.space", - "the address space could not be cloned in this environment"); - return; - } - observe(kind::behaviour, true, "the calling address space is cloned"); - - // A CONTEXT STARTED IN THE CLONE IS A PROCESS, which is why kal_process_wait - // applies to it unchanged rather than being restated in this interface. That - // it is waitable is the observation; what it computed is not, because the - // clone is a separate space and the caller cannot see its stores. + // 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. { - volatile int flag = 0; + int marker = 0; kal_process p{}; - const int src = kal_space_start(sp, &entry, (void*)&flag, + const int src = kal_space_start(&entry, static_cast(&marker), child_stack + sizeof child_stack, &p); - observe(kind::behaviour, src == kal_ok, - "a context starts in the cloned space"); - if (src == kal_ok) { - 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"); - kal_process_close(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"); - // THE STORE IS NOT VISIBLE HERE, AND THAT IS THE POINT. A clone is a - // separate address space; a caller observing its own flag set would be - // observing a shared space, which is openkal.task and not this - // interface. - observe(kind::behaviour, flag == 0, - "a store in the cloned space is not observed in the original"); - } + 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_space_destroy(sp); + kal_process_close(p); - // The two handles are independent and either may be closed first. Destroying - // a space whose context has already ended is the ordinary order and is - // observed by the absence of a failure in the run that follows. + // 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_space second{}; - const int rc = kal_space_clone(&second); + kal_process p{}; + const int rc = kal_space_start(&entry, nullptr, + child_stack + sizeof child_stack, &p); if (rc == kal_ok) { - kal_space_destroy(second); - observe(kind::behaviour, true, - "a cloned space with no context started in it is released"); + 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 space with no context is released", - "a second clone could not be made"); + unobserved(kind::behaviour, "a second context starts", + "the second start was refused"); } } #endif diff --git a/include/openkal/space.h b/include/openkal/space.h index 37a666e..22a36c0 100644 --- a/include/openkal/space.h +++ b/include/openkal/space.h @@ -1,75 +1,82 @@ -/* openkal.space --- an address space, and a context executing in one. +/* 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. - * 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. + * 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 clone and restoring it in the new context. That is + * 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. * - * THERE IS NO `create'. 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. */ + * 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" -/* An opaque handle occupying one machine word, per clause 7.2. */ -struct kal_space { kal_uintptr h; }; - /* Positions in kal_space_props. */ -/* Whether a clone carries the handles the original holds. +/* Whether the started context holds the handles the caller holds. * - * An environment whose cloning primitive copies memory and not handles reports + * 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 cloned memory - * may fail after the clone has already reported success. +/* 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 it 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. */ + * 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 -/* Clones the CALLER's address space. */ -int kal_space_clone(struct kal_space* out); - -/* Starts a context in the given space. +/* 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. */ -int kal_space_start(struct kal_space sp, - void (*entry)(void*), void* arg, void* stack_top, + * 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); -/* Releases the space. - * - * A space whose context is running is not released until that process ends. The - * two handles are independent and either may be closed first, which is the same - * ownership rule clause 6.7 states generally. */ -void kal_space_destroy(struct kal_space sp); - extern const kal_uintptr kal_space_props; #ifdef __cplusplus diff --git a/src/space.cppm b/src/space.cppm index a126fe4..09d7faf 100644 --- a/src/space.cppm +++ b/src/space.cppm @@ -7,10 +7,18 @@ // stated in a C application binary interface at all. // // A library above this interface reaches fork by saving its own execution state -// before the clone and restoring it in the new context. That is composition, +// 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 @@ -18,19 +26,11 @@ export module openkal.space; export import openkal.types; export import openkal.process; -export using ::kal_space; - -export using ::kal_space_clone; export using ::kal_space_start; -export using ::kal_space_destroy; export using ::kal_space_props; -static_assert(sizeof(kal_space) == sizeof(kal_uintptr), "clause 7.2"); - export namespace kal::space { -using space = kal_space; - struct props_tag; using props = kal::props; @@ -40,21 +40,12 @@ 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 clone_result { space sp; int e; }; -struct process_result { kal_process p; int e; }; +struct process_result { kal_process p; int e; }; -inline clone_result clone() { - space sp{}; - const int e = kal_space_clone(&sp); - return { sp, e }; -} - -inline process_result start(space sp, void (*entry)(void*), void* arg, void* stack_top) { +inline process_result start(void (*entry)(void*), void* arg, void* stack_top) { kal_process p{}; - const int e = kal_space_start(sp, entry, arg, stack_top, &p); + const int e = kal_space_start(entry, arg, stack_top, &p); return { p, e }; } -inline void destroy(space sp) { kal_space_destroy(sp); } - } From fc1227ef4c2f61b8a605a0339e924f117837000c Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 13:22:28 +0800 Subject: [PATCH 3/9] spec: a connection is an owned handle whose stream is borrowed Found by writing the first implementation, which is where a defect of this kind becomes visible: reading the interface did not reveal it, and building it did on the first run. openkal.net yielded a kal_stream and declared it owned. That cannot be implemented under clause 7.2, which requires that a released handle not be treated as valid. 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 --- so there is nowhere to carry the generation that makes a released handle detectable. An owned handle can carry one; a borrowed stream cannot. openkal.fs had already answered the same question. A file is owned, its stream is borrowed from it through kal_fs_stream, and the stream is released with the file rather than separately. openkal.net now has that shape exactly, with the same wording, because it is the same arrangement. added struct kal_net_conn, kal_uintptr kal_net_stream(struct kal_net_conn) changed connect, accept, peer, local, shutdown and close take a connection changed kal_timeout_accept yields a connection The first form also failed a plainer test. The Linux implementation packed the connection into the handle scheme, kal_stream_read took the packed word as a descriptor, and the transfer read nothing: FAIL: the bytes read are the bytes written FAIL: the peer observes end of input after a half-closure Both observations hold under the corrected shape. The conformance sections name the properties through the modules rather than through the macros. A macro does not cross a module boundary, and a section is a module; the C form's KAL_ names are for a consumer written in C. declarations 90 names, compiled without the environment's headers types 149 declarations, no backend type named conformance 100 observations held, 0 did not hold, against openkal-linux --- SURFACE.txt | 1 + conformance/src/declarations.c | 1 + conformance/src/sections/net.cpp | 25 ++++++++------ conformance/src/sections/space.cpp | 4 +-- conformance/src/sections/terminal.cpp | 10 +++--- include/openkal/net.h | 47 +++++++++++++++++++-------- include/openkal/timeout.h | 2 +- src/net.cppm | 46 +++++++++++++++----------- src/timeout.cppm | 10 +++--- src/types.cppm | 10 ++++++ 10 files changed, 101 insertions(+), 55 deletions(-) diff --git a/SURFACE.txt b/SURFACE.txt index 6817519..25731fc 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -94,6 +94,7 @@ kal_net_local kal_net_peer kal_net_props kal_net_shutdown +kal_net_stream # openkal.datagram kal_datagram_close kal_datagram_local diff --git a/conformance/src/declarations.c b/conformance/src/declarations.c index 42ade8c..7f62254 100644 --- a/conformance/src/declarations.c +++ b/conformance/src/declarations.c @@ -75,6 +75,7 @@ void okc_declarations_c(void) (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); diff --git a/conformance/src/sections/net.cpp b/conformance/src/sections/net.cpp index defa8dd..05e5d2d 100644 --- a/conformance/src/sections/net.cpp +++ b/conformance/src/sections/net.cpp @@ -64,24 +64,29 @@ void run() { // 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_stream client{}; + 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_stream server{}; + 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(client, msg, sizeof msg - 1); + 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(server, buf, sizeof buf); + 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; @@ -104,13 +109,13 @@ void run() { // are conforming; what would not be conforming is claiming the position and // then refusing. { - const bool claims = (kal_net_props & KAL_NET_PROP_HALFCLOSE) != 0; - const int rc = kal_net_shutdown(client, KAL_SHUT_WRITE); + 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(server, buf, sizeof buf); + 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 { @@ -131,11 +136,11 @@ void run() { kal_endpoint odd{}; odd.addr_len = 7; // not 4, not 16, not 20 odd.port = 9; - kal_stream s{}; - const int rc = kal_net_connect(&odd, &s); + 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(s); + if (rc == kal_ok) kal_net_close(c); } #endif } diff --git a/conformance/src/sections/space.cpp b/conformance/src/sections/space.cpp index 8dd9803..2aab173 100644 --- a/conformance/src/sections/space.cpp +++ b/conformance/src/sections/space.cpp @@ -45,8 +45,8 @@ void run() { // 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_props & KAL_SPACE_PROP_CLONE_HANDLES) != 0; - const bool deferred = (kal_space_props & KAL_SPACE_PROP_DEFERRED_COPY) != 0; + 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" diff --git a/conformance/src/sections/terminal.cpp b/conformance/src/sections/terminal.cpp index 2b6d4f4..65ebb99 100644 --- a/conformance/src/sections/terminal.cpp +++ b/conformance/src/sections/terminal.cpp @@ -19,9 +19,11 @@ void run() { // 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(out) & KAL_STREAM_PROP_INTERACTIVE) != 0; + kal::stream_props{kal_stream_props(out)}.has(kal::stream_prop::interactive); if (!interactive) { unobserved(kind::behaviour, "openkal.terminal", @@ -103,13 +105,13 @@ void run() { // correspond to them must be answered. A word claiming a facility the // implementation refuses is the disagreement clause 6.2 exists to prevent. { - const kal_uintptr props = kal_terminal_props(out); - if ((props & KAL_TERM_PROP_SIZE) != 0) { + 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 & KAL_TERM_PROP_MODE) != 0) { + 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"); diff --git a/include/openkal/net.h b/include/openkal/net.h index 226f6c3..911022c 100644 --- a/include/openkal/net.h +++ b/include/openkal/net.h @@ -21,9 +21,22 @@ #include "types.h" #include "stream.h" -/* An opaque handle occupying one machine word, per clause 7.2. A listener is - * not a stream: nothing is transferred through it, and giving it the stream - * operations would be an interface whose resource can never satisfy them. */ +/* 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. */ @@ -39,9 +52,14 @@ struct kal_net_listener { kal_uintptr h; }; extern "C" { #endif -int kal_net_connect(const struct kal_endpoint* to, struct kal_stream* out); +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_stream* 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. * @@ -49,8 +67,8 @@ int kal_net_accept (struct kal_net_listener l, struct kal_stream* out); * 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_stream s, struct kal_endpoint* out); -int kal_net_local (struct kal_stream s, struct kal_endpoint* out); +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. @@ -60,14 +78,15 @@ int kal_net_listener_local(struct kal_net_listener l, struct kal_endpoint* out); * 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_stream s, int direction); +int kal_net_shutdown(struct kal_net_conn c, int direction); -/* AN OWNED STREAM, UNLIKE THE THREE `openkal.stream' PROVIDES. A connection is - * obtained and must be released; the standard streams are borrowed and are not. - * This is the same division `openkal.fs' already draws with - * `kal_fs_close_file', and the reason the release lives here rather than in - * `openkal.stream': a stream in general has no owner to return it to. */ -void kal_net_close (struct kal_stream s); +/* 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. diff --git a/include/openkal/timeout.h b/include/openkal/timeout.h index 9488c53..a157490 100644 --- a/include/openkal/timeout.h +++ b/include/openkal/timeout.h @@ -37,7 +37,7 @@ extern "C" { 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_stream* out); +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); diff --git a/src/net.cppm b/src/net.cppm index 5f58eab..a598fd5 100644 --- a/src/net.cppm +++ b/src/net.cppm @@ -18,11 +18,13 @@ 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; @@ -31,10 +33,12 @@ 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; @@ -51,21 +55,21 @@ inline bool has(props p) { return properties().has(p); } // 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(kal_stream s, shut d) { - return kal_net_shutdown(s, static_cast(d)); +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 stream_result { kal_stream s; int e; }; -struct listener_result { listener l; int e; }; -struct endpoint_result { endpoint ep; int e; }; - -inline stream_result connect(const endpoint& to) { - kal_stream s{}; - const int e = kal_net_connect(&to, &s); - return { s, e }; +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) { @@ -74,17 +78,21 @@ inline listener_result listen(const endpoint& local) { return { l, e }; } -inline stream_result accept(listener l) { - kal_stream s{}; - const int e = kal_net_accept(l, &s); - return { s, e }; +inline conn_result accept(listener l) { + conn c{}; + const int e = kal_net_accept(l, &c); + return { c, e }; } -inline endpoint_result peer (kal_stream s) { endpoint ep{}; const int e = kal_net_peer (s, &ep); return { ep, e }; } -inline endpoint_result local(kal_stream s) { endpoint ep{}; const int e = kal_net_local(s, &ep); return { ep, e }; } -inline endpoint_result local(listener l) { endpoint ep{}; const int e = kal_net_listener_local(l, &ep); return { ep, 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 void close(kal_stream s) { kal_net_close(s); } -inline void close(listener l) { kal_net_close_listener(l); } +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/timeout.cppm b/src/timeout.cppm index 86a378d..8ab82f1 100644 --- a/src/timeout.cppm +++ b/src/timeout.cppm @@ -45,12 +45,12 @@ inline kal_io_result write(kal_stream s, const void* p, kal_uintptr n, kal_u64 n return kal_timeout_write(s, p, n, ns); } -struct stream_result { kal_stream s; int e; }; +struct conn_result { kal_net_conn c; int e; }; -inline stream_result accept(kal_net_listener l, kal_u64 ns) { - kal_stream s{}; - const int e = kal_timeout_accept(l, ns, &s); - return { s, 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 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. From c4030db8c544aafb95820b92a868098d521a040d Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 13:52:42 +0800 Subject: [PATCH 4/9] Add openkal-kit: what is composed, kept out of what is specified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 keeps openkal implementable on a machine with firmware and nothing else, and it leaves a gap: a program wanting 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 none here, because both are 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 is where it is written once. ## The contract form decides what is normative Clause 10 states that openkal's contract is a C application binary interface. The kit deliberately is not one: C++ modules in `namespace kal::kit`, exporting no name beginning with `kal_`. A C++ name is mangled to `_ZN3kal3kit…`, so `check-surface.sh --complete` does not read a program that links the kit as an implementation which has added names. "Is this normative" is therefore 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 that the kit MAY evolve, which clause 8 forbids the specification from doing. ## What is in it openkal.kit.endpoint writes an endpoint down and reads one back. Clause 3.4 excludes name resolution from the specification, and that exclusion 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. The rejections are what makes it worth having --- a parser accepting "1.2.3" as 1.2.0.3, or "300.1.1.1" as 44.1.1.1, turns a mistyped configuration into a connection somewhere else and reports nothing at the time. openkal.kit.channel carries bytes between two contexts of one address space, from a region of openkal.memory and openkal.task's wait and wake. It is deliberately not kal_process_channel: that interface crosses a spawn boundary and is a kernel facility because a child cannot be handed a pointer into its parent's memory. This one needs nothing from the kernel but a place to sleep. Two details are load-bearing and are recorded where they are made. The close is tested AFTER the indices, because a writer that filled the ring and closed in the same breath leaves bytes behind it and a reader seeing the close first would report an end of input that lost data. Both indices are woken on close, because a writer waiting for room and a reader waiting for bytes must both learn that no more is coming. ## Verification The tests are behavioural. A test that named each entity and did nothing with it would compile against a package whose every operation did nothing. The channel is exercised with a hundred thousand bytes through a four-kilobyte ring, across two contexts, in blocks that are not a divisor of the ring. A payload that fitted would never make the writer wait, and the sleeping and waking the module exists for would go unobserved. openkal-kit: every observation held tools/run-kit-tests.sh substitutes working trees for both the specification and the implementation, as run-conformance.sh does and for the same reason: a change spanning two repositories must be tested against both halves as written. The substitution is asserted rather than assumed --- one that matched nothing would leave the manifest naming a published version, and the run would report on that one while appearing to report on this branch --- and a trap restores the manifests, because leaving a local absolute path in a public repository is a mistake this repository has made once already. --- .github/workflows/ci.yml | 18 ++++ README.md | 35 ++++++++ kit/mcpp.toml | 71 +++++++++++++++ kit/src/channel.cppm | 189 +++++++++++++++++++++++++++++++++++++++ kit/src/endpoint.cppm | 144 +++++++++++++++++++++++++++++ kit/tests/kit.cpp | 155 ++++++++++++++++++++++++++++++++ tools/run-kit-tests.sh | 71 +++++++++++++++ 7 files changed, 683 insertions(+) create mode 100644 kit/mcpp.toml create mode 100644 kit/src/channel.cppm create mode 100644 kit/src/endpoint.cppm create mode 100644 kit/tests/kit.cpp create mode 100755 tools/run-kit-tests.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d368f0..a220998 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -717,6 +717,24 @@ 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 + - 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..a4f02cc 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,38 @@ 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_`. A C++ name is mangled to `_ZN3kal3kit…`, 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/kit/mcpp.toml b/kit/mcpp.toml new file mode 100644 index 0000000..0532893 --- /dev/null +++ b/kit/mcpp.toml @@ -0,0 +1,71 @@ +[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_`. A C++ name is mangled to +# `_ZN3kal3kit…`, so `tools/check-surface.sh --complete` does not read this +# package as an implementation that has added names --- the rule it enforces is +# about the C surface, and this package 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: 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/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 "$@" ) From 8f7f9f9cc51a99915427c3ef9d29f6e9fe5b6dbb Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 14:04:49 +0800 Subject: [PATCH 5/9] conformance: examine the three operations added to openkal.process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They are examined in that interface's section rather than in one of their own, because they are part of it. Clause 6.1 makes an interface provided IN PART a deviation, so an implementation reaching this section has undertaken to provide them and the suite is entitled to observe them. The observations are of behaviour: a channel carries bytes, and closing the far end is observed as end of input on the near one. That last is what the release exists for --- a parent which does not release the far end after a spawn never observes the end of input, which is the deadlock this pair invites and the reason the release is declared beside the operation rather than left to openkal.stream. ⚠️ THE ABI CHECK WOULD HAVE FAILED, AND ONLY WITH `abi` SELECTED. It compares the capability word against the set of positions the specification has assigned, and that set was written out by name: DID NOT HOLD [abi] the capability word contains no position the specification has not assigned An implementation claiming the two positions version 0.8 adds was therefore reported as claiming something unassigned. Measured by narrowing the set again and watching the observation fail, so that the widening is known to be what carries it rather than assumed. That is the second time in this release that adding to an existing interface reached further than adding a new one. The first was the surface checker, which found three backends providing openkal.process in part; this one is in the suite those backends run against themselves. 143 observations held, 0 did not hold, with full,optional and the five interfaces version 0.8 adds --- conformance/src/sections/process.cpp | 51 +++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) 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"); } From 00a59fff0d90529d7e6d3ad14aade589da810dda Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 15:23:37 +0800 Subject: [PATCH 6/9] kit: state the surface claim as it was measured, not as it was reasoned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The claim is that the kit cannot be mistaken for an implementation because it exports no name beginning with kal_, and the claim holds: measured on its objects, zero of the defined names do. ⚠️ The REASON given was wrong in its particulars. It said a C++ name is mangled to `_ZN3kal3kit…`, and these objects contain no such symbol: the operations are inline and are emitted into consumers rather than exported, so what the objects define are module initialisers like `_ZGIW7openkalW3kitW7channel`. An argument that rests on how a compiler mangles names is worth checking against what the compiler emitted. The structural conclusion is unchanged --- C++ mangling cannot produce a bare kal_ symbol --- but a reader following the comment to verify it would have looked for something that is not there. --- README.md | 4 +++- kit/mcpp.toml | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a4f02cc..e80dd48 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,9 @@ 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_`. A C++ name is mangled to `_ZN3kal3kit…`, so +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. diff --git a/kit/mcpp.toml b/kit/mcpp.toml index 0532893..1839cfe 100644 --- a/kit/mcpp.toml +++ b/kit/mcpp.toml @@ -25,11 +25,19 @@ repo = "https://github.com/mcpplibs/openkal" # # 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_`. A C++ name is mangled to -# `_ZN3kal3kit…`, so `tools/check-surface.sh --complete` does not read this -# package as an implementation that has added names --- the rule it enforces is +# 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. From 46e53cddbcd465008c1b79769f30f04e9486c838 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 17:20:17 +0800 Subject: [PATCH 7/9] Build the portable program, and pin the released engine Three changes, and the second is why the first two were needed. THE ENGINE. MCPP_VERSION moves to 2026.8.27.1, which is now published, and the repository variable MCPP_SOURCE_REF is cleared, so this repository tests the released engine rather than a branch of it. THE EXAMPLE HAD DRIFTED THREE SPECIFICATION VERSIONS. examples/portable named `openkal = "0.5.0"` while declaring itself the program that demonstrates the current contract to a reader who would copy it. The backends it named were equally old. NOTHING BUILT IT, WHICH IS WHY. 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` appears in none of their workflows. The sentence described an arrangement that existed nowhere, and a claim with no criterion behind it cannot notice its own subject going stale. The conformance job now builds and runs it against each implementation it tests, substituting the working trees as the conformance runner already does. The criterion carries a denominator: a program that failed to start would print none of its lines, and a check for the absence of failures would hold over an empty log, so the line count is asserted before the count of failures. Measured before the step was written --- the source needed no change, only its manifest: over openkal 0.8 and openkal-linux as written here it prints all eight interface lines and `observations that did not hold: 0`. Two details the measurement settled. The substitution uses relative paths, because this job runs on Windows where MSYS reports `/d/a/...`, which the build tool does not resolve, and because a relative path cannot put one machine's directory into a file that might be committed. It passes no `standalone` feature: that feature makes the implementation supply `_start`, which collides with the C library's crt1.o on a hosted toolchain --- `multiple definition of _start`, measured --- and the conformance runner defaults to no features for the same reason. --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++++++++++- examples/portable/README.md | 14 ++++++---- examples/portable/mcpp.toml | 8 +++--- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a220998..4da0969 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' @@ -735,6 +735,56 @@ jobs: 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; } + + 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/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" From 271d20c18965aa5e89e9c6955624f740078859c9 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 17:22:23 +0800 Subject: [PATCH 8/9] Substitute the specification on both routes to it The portable step rewrote the example's manifest and not the implementation's, so one package was reachable two ways and the build refused: error: dependency 'mcpplibs.openkal' is requested as both a path dep (by 'portable') and a git dep (by 'mcpplibs.openkal-linux@path'). The refusal is correct --- two routes to one package are two versions of it --- and the omission was mine rather than the example's. The local measurement that preceded this step did substitute both; only the step did not. Both manifests now name the same directory by relative path, as tools/run-conformance.sh already does for the suite, and each substitution is asserted rather than assumed. --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4da0969..034c79b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -773,6 +773,20 @@ jobs: 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 From fe43bf6721aa5072f6dd381e43e3314ed9641965 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 17:27:43 +0800 Subject: [PATCH 9/9] The portable program names no compiler either MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building it for the first time found that it did not compile under MSVC: error C3861: '__atomic_load_n': identifier not found error C2065: '__ATOMIC_RELAXED': undeclared identifier error C2065: '__UINT64_TYPE__': undeclared identifier ⚠️ NAMING NO SYSTEM CALL IS NOT THE SAME AS NAMING NO COMPILER. The program's opening sentence claims it names no implementation, no operating system, no descriptor number and no system call, and all of that was true. It was written in GCC and Clang spellings throughout, so the one program whose premise is portability failed on one of the three compilers its own specification is tested against. Two of the five conformance rows would have caught it on any day since it was written; neither ever built it. The integer types become 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. `kal_task_wait` takes `const kal_u32*`, which the lock word now is by declaration rather than by coincidence. The atomics become the language's. `std::atomic_ref` refers to the existing word rather than replacing it, which is what allows `kal_task_wait` to take its address, and it 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; an atomic operation is a language facility and openkal is right not to. Measured over openkal-linux before pushing: all eight interface lines and `observations that did not hold: 0`, including the contended-counter observation, which is the one that exercises the rewritten lock rather than merely compiling it. --- examples/portable/src/main.cpp | 41 +++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) 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);