diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07e2dd8..6eae1d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ on: default: "" env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }} - MCPP_VERSION: 2026.8.26.2 + MCPP_VERSION: 2026.8.27.1 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' @@ -49,6 +49,31 @@ jobs: # The specification is checked out at the branch under test where it has # one, so that this run asserts what it is for: that the specification as # written there and this implementation as written here agree today. + # THE COMMITTED MANIFEST NAMES NO DIRECTORY OF ANYBODY'S MACHINE. + # + # Two scripts in the specification's repository rewrite this manifest to + # name a working tree --- run-conformance.sh and run-kit-tests.sh --- and + # both restore it through a trap. A trap does not fire when the process is + # killed, and a run by hand followed by `git add -A` then publishes a path + # that exists on one machine: a consumer resolving from the index is handed + # a manifest pointing at a directory that exists nowhere. + # + # ⚠️ THAT HAS HAPPENED IN THIS ECOSYSTEM, in openkal-musl, and the working + # tree here has carried the same rewrite more than once since. This step + # runs first, so what it examines is what the commit contains. + - name: The committed manifest names no local directory + run: | + set -euo pipefail + bad=$(grep -nE '^[a-z-]+ = \{[^}]*path = "(/|[A-Za-z]:)' mcpp.toml || true) + if [ -n "$bad" ]; then + echo "::error::the committed manifest names an absolute path" + printf '%s\n' "$bad" | sed 's/^/ /' + echo " run 'git checkout -- mcpp.toml' after using the" + echo " specification's conformance or kit scripts by hand." + exit 1 + fi + echo " ok every dependency is named by version, branch or a relative path" + - name: The specification run: | git clone --quiet https://github.com/mcpplibs/openkal.git .spec diff --git a/.gitignore b/.gitignore index ff39a22..08ea216 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ compile_commands.json # What a system leaves behind. .DS_Store Thumbs.db + +# The specification tree tools/run-conformance.sh clones beside the sources. +.spec/ diff --git a/mcpp.toml b/mcpp.toml index f4583c7..2aebe52 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.5.4" +version = "0.6.0" description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-linux" [dependencies] -openkal = "0.7.0" +openkal = "0.8.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/src/datagram.cpp b/src/datagram.cpp new file mode 100644 index 0000000..1569f60 --- /dev/null +++ b/src/datagram.cpp @@ -0,0 +1,151 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include + +// openkal.datagram upon the kernel's socket calls. +// +// A DATAGRAM IS NOT PACKED AS A kal_stream, and the handle type is its own for +// that reason: kal_stream_read reports a count and not a boundary, so reading a +// datagram through it would lose the property that distinguishes this interface. +// The packing is the same, the type is not, and the type is what prevents the +// mistake. + +namespace { + +int fd_of(kal_datagram d) { return okl::unpack(d.h); } + +} // namespace + +extern "C" { + +int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) { + if (out == nullptr) return kal_err_invalid; + + // A null local endpoint asks for one that may send and whose receiving + // address is unspecified. IPv4 is chosen for it, because a family must be + // named at the point the socket is made and this is the one every + // environment that has a network at all provides. + okl_long family = okl::af_inet; + if (local != nullptr) { + family = okl::family_of(*local); + if (family < 0) return kal_err_invalid; + } + + const okl_long fd = okl::sys(okl::nr_socket, family, + okl::sock_dgram | okl::sock_cloexec, + okl::ipproto_udp); + if (okl::failed(fd)) return okl::translate(fd); + + if (local != nullptr) { + okl::ksockaddr_storage ss{}; + okl_long len = 0; + if (const int rc = okl::to_kernel(*local, ss, len); rc != kal_ok) { + okl::sys(okl::nr_close, fd); + return rc; + } + if (const okl_long r = okl::sys(okl::nr_bind, fd, + reinterpret_cast(&ss), len); + okl::failed(r)) { + okl::sys(okl::nr_close, fd); + return okl::translate(r); + } + } + + out->h = okl::pack(static_cast(fd)); + if (out->h == 0) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_datagram_local(kal_datagram d, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = fd_of(d); + if (fd < 0) return kal_err_invalid; + + okl::ksockaddr_storage ss{}; + okl_long len = static_cast(sizeof ss); + const okl_long r = okl::sys(okl::nr_getsockname, fd, + reinterpret_cast(&ss), + reinterpret_cast(&len)); + if (okl::failed(r)) return okl::translate(r); + return okl::from_kernel(ss, *out); +} + +kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len, + const kal_endpoint* to) { + const int fd = fd_of(d); + if (fd < 0 || to == nullptr) return { 0, kal_err_invalid }; + + okl::ksockaddr_storage ss{}; + okl_long addrlen = 0; + if (const int rc = okl::to_kernel(*to, ss, addrlen); rc != kal_ok) + return { 0, rc }; + + for (;;) { + const okl_long r = okl::sys(okl::nr_sendto, fd, + reinterpret_cast(buf), + static_cast(len), 0, + reinterpret_cast(&ss), addrlen); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return { 0, okl::translate(r) }; + + // A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface + // states. The kernel reports a count anyway; a count short of the length + // would mean the medium had split the message, which for a datagram + // socket it does not do. Reporting the short count as success would give + // a caller a partial send this interface says cannot occur, so it is + // reported as a failure of the medium instead. + const kal_uintptr n = static_cast(r); + return { n, n == len ? kal_ok : kal_err_io }; + } +} + +kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from) { + const int fd = fd_of(d); + if (fd < 0) return { 0, kal_err_invalid }; + + okl::ksockaddr_storage ss{}; + okl_long addrlen = static_cast(sizeof ss); + + for (;;) { + const okl_long r = okl::sys(okl::nr_recvfrom, fd, + reinterpret_cast(buf), + static_cast(len), 0, + reinterpret_cast(&ss), + reinterpret_cast(&addrlen)); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return { 0, okl::translate(r) }; + + // THE COUNT REPORTED IS WHAT WAS PLACED IN THE BUFFER, not what was + // sent. Without MSG_TRUNC the kernel already reports the former, which + // is what this interface requires: a caller that trusted the larger + // number would read beyond its own buffer. + if (from != nullptr) { + // A sender whose family this implementation does not know leaves the + // endpoint zeroed rather than partly filled. The transfer still + // happened and is reported; what is unknown is who sent it. + if (okl::from_kernel(ss, *from) != kal_ok) { + for (auto& b : from->addr) b = 0; + from->addr_len = 0; + from->port = 0; + } + } + return { static_cast(r), kal_ok }; + } +} + +void kal_datagram_close(kal_datagram d) { + const int fd = fd_of(d); + if (fd < 0) return; + okl::sys(okl::nr_close, fd); + okl::retire(d.h); +} + +// Broadcast is not claimed. The kernel provides it only after SO_BROADCAST has +// been set, and this interface has no operation that would set it; a word +// claiming a facility no operation reaches is the disagreement clause 6.2 exists +// to prevent. +const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6; + +} // extern "C" diff --git a/src/endpoint.h b/src/endpoint.h new file mode 100644 index 0000000..37ad624 --- /dev/null +++ b/src/endpoint.h @@ -0,0 +1,112 @@ +// Conversion between kal_endpoint and the kernel's socket address structures. +// +// SHARED BY openkal.net AND openkal.datagram BECAUSE THE TYPE IS. Either +// interface may be provided without the other, so the conversion belongs to +// neither; writing it twice would be one decision stated in two places, and the +// two would eventually disagree about which lengths are accepted. +#pragma once +#include "sys.h" +#include + +namespace okl { + +// The port is carried in host order by kal_endpoint and in network order by the +// kernel. The conversion is written out rather than taken from a C library's +// htons, for the reason the head of sys.h gives. +inline unsigned short to_net_port(kal_u32 port) { + const unsigned short p = static_cast(port & 0xffffu); + return static_cast((p << 8) | (p >> 8)); +} +inline kal_u32 from_net_port(unsigned short net) { + return static_cast((net << 8) | (net >> 8)) & 0xffffu; +} + +// Fills a kernel address from an endpoint, and reports its length. +// +// A LENGTH THIS IMPLEMENTATION DOES NOT KNOW IS REFUSED RATHER THAN READ AS ONE +// IT DOES. The specification defines the set of lengths and allows it to grow; +// an implementation that ignored the field would misread every address a later +// revision defines, and would do so silently. +inline int to_kernel(const kal_endpoint& ep, ksockaddr_storage& out, okl_long& len) { + for (auto& b : out.pad) b = 0; + + if (ep.addr_len == 4) { + auto* v4 = reinterpret_cast(&out); + v4->family = af_inet; + v4->port = to_net_port(ep.port); + okl_u32 a = 0; + for (int i = 0; i < 4; ++i) + a |= static_cast(ep.addr[i]) << (i * 8); // already network order + v4->addr = a; + for (auto& z : v4->zero) z = 0; + len = static_cast(sizeof(ksockaddr_in)); + return kal_ok; + } + + // Sixteen bytes is an address; twenty is an address followed by a scope + // identifier, which is carried in the four bytes after it. + if (ep.addr_len == 16 || ep.addr_len == 20) { + auto* v6 = reinterpret_cast(&out); + v6->family = af_inet6; + v6->port = to_net_port(ep.port); + v6->flowinfo = 0; + for (int i = 0; i < 16; ++i) v6->addr[i] = ep.addr[i]; + okl_u32 scope = 0; + if (ep.addr_len == 20) + for (int i = 0; i < 4; ++i) + scope |= static_cast(ep.addr[16 + i]) << (i * 8); + v6->scope_id = scope; + len = static_cast(sizeof(ksockaddr_in6)); + return kal_ok; + } + + return kal_err_invalid; +} + +// Fills an endpoint from a kernel address. A family this implementation does +// not know leaves the endpoint zeroed and reports it, for the same reason. +inline int from_kernel(const ksockaddr_storage& in, kal_endpoint& out) { + for (auto& b : out.addr) b = 0; + out.addr_len = 0; + out.port = 0; + + if (in.family == af_inet) { + const auto* v4 = reinterpret_cast(&in); + const okl_u32 a = v4->addr; + for (int i = 0; i < 4; ++i) + out.addr[i] = static_cast((a >> (i * 8)) & 0xffu); + out.addr_len = 4; + out.port = from_net_port(v4->port); + return kal_ok; + } + + if (in.family == af_inet6) { + const auto* v6 = reinterpret_cast(&in); + for (int i = 0; i < 16; ++i) out.addr[i] = v6->addr[i]; + // A zero scope identifier is reported as the shorter form. The two + // lengths denote the same address when the scope is zero, and reporting + // the shorter one keeps an address that came in as sixteen bytes going + // back out as sixteen. + if (v6->scope_id == 0) { + out.addr_len = 16; + } else { + for (int i = 0; i < 4; ++i) + out.addr[16 + i] = static_cast((v6->scope_id >> (i * 8)) & 0xffu); + out.addr_len = 20; + } + out.port = from_net_port(v6->port); + return kal_ok; + } + + return kal_err_invalid; +} + +// Which socket family an endpoint asks for, or -1 for a length that is not one +// of the defined ones. +inline okl_long family_of(const kal_endpoint& ep) { + if (ep.addr_len == 4) return af_inet; + if (ep.addr_len == 16 || ep.addr_len == 20) return af_inet6; + return -1; +} + +} // namespace okl diff --git a/src/net.cpp b/src/net.cpp new file mode 100644 index 0000000..aab1754 --- /dev/null +++ b/src/net.cpp @@ -0,0 +1,191 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include + +// openkal.net upon the kernel's socket calls. +// +// A CONNECTION IS AN OWNED HANDLE AND THE STREAM IS BORROWED FROM IT, exactly as +// kal_file and kal_fs_stream are here. The owned handle carries a generation so +// that a released one stops being valid, which clause 7.2 requires; the stream +// it yields is the bare descriptor, because that is what openkal.stream's +// transfer operations take. The interface was changed to this shape after the +// first form --- an owned kal_stream --- turned out not to be implementable +// under clause 7.2 at all. + +namespace { + +// Both handle kinds are descriptors, and both use the packing in handle.h. The +// generation makes a released handle stop being valid, which clause 7.2 +// requires and which a bare descriptor could not provide: the kernel reuses the +// lowest free number, so a stale word would name whatever was opened next. +int fd_of(kal_net_conn c) { return okl::unpack(c.h); } +int fd_of(kal_net_listener l) { return okl::unpack(l.h); } + +int report_address(okl_long call, int fd, kal_endpoint* out) { + if (out == nullptr) return kal_err_invalid; + if (fd < 0) return kal_err_invalid; + okl::ksockaddr_storage ss{}; + okl_long len = static_cast(sizeof ss); + const okl_long r = okl::sys(call, fd, reinterpret_cast(&ss), + reinterpret_cast(&len)); + if (okl::failed(r)) return okl::translate(r); + return okl::from_kernel(ss, *out); +} + +} // namespace + +extern "C" { + +int kal_net_connect(const kal_endpoint* to, kal_net_conn* out) { + if (to == nullptr || out == nullptr) return kal_err_invalid; + const okl_long family = okl::family_of(*to); + if (family < 0) return kal_err_invalid; + + okl::ksockaddr_storage ss{}; + okl_long len = 0; + if (const int rc = okl::to_kernel(*to, ss, len); rc != kal_ok) return rc; + + const okl_long fd = okl::sys(okl::nr_socket, family, + okl::sock_stream | okl::sock_cloexec, + okl::ipproto_tcp); + if (okl::failed(fd)) return okl::translate(fd); + + for (;;) { + const okl_long r = okl::sys(okl::nr_connect, fd, + reinterpret_cast(&ss), len); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) { + okl::sys(okl::nr_close, fd); + return okl::translate(r); + } + break; + } + + out->h = okl::pack(static_cast(fd)); + if (out->h == 0) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_listen(const kal_endpoint* local, kal_net_listener* out) { + if (local == nullptr || out == nullptr) return kal_err_invalid; + const okl_long family = okl::family_of(*local); + if (family < 0) return kal_err_invalid; + + okl::ksockaddr_storage ss{}; + okl_long len = 0; + if (const int rc = okl::to_kernel(*local, ss, len); rc != kal_ok) return rc; + + const okl_long fd = okl::sys(okl::nr_socket, family, + okl::sock_stream | okl::sock_cloexec, + okl::ipproto_tcp); + if (okl::failed(fd)) return okl::translate(fd); + + // SO_REUSEADDR, because a listener whose predecessor is in the kernel's + // lingering state would otherwise be refused for a reason that has nothing + // to do with the caller. A program restarted within the linger interval is + // the ordinary case, not an unusual one. + { + const int on = 1; + okl::sys(okl::nr_setsockopt, fd, okl::sol_socket, okl::so_reuseaddr, + reinterpret_cast(&on), + static_cast(sizeof on)); + } + + if (const okl_long r = okl::sys(okl::nr_bind, fd, + reinterpret_cast(&ss), len); + okl::failed(r)) { + okl::sys(okl::nr_close, fd); + return okl::translate(r); + } + + // The backlog the kernel is asked for. A number rather than a name, because + // this interface does not expose one and a caller has no way to state it. + if (const okl_long r = okl::sys(okl::nr_listen, fd, 128); okl::failed(r)) { + okl::sys(okl::nr_close, fd); + return okl::translate(r); + } + + out->h = okl::pack(static_cast(fd)); + if (out->h == 0) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_accept(kal_net_listener l, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = fd_of(l); + if (fd < 0) return kal_err_invalid; + + for (;;) { + const okl_long r = okl::sys(okl::nr_accept4, fd, 0, 0, + okl::sock_cloexec); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return okl::translate(r); + out->h = okl::pack(static_cast(r)); + if (out->h == 0) { okl::sys(okl::nr_close, r); return kal_err_no_memory; } + return kal_ok; + } +} + +kal_uintptr kal_net_stream(kal_net_conn c) { + // The bare descriptor, for the reason kal_fs_stream gives: openkal.stream's + // operations take whatever the environment's transfer calls take, and a + // packed word is not that. + const int fd = fd_of(c); + return fd < 0 ? 0u : static_cast(fd); +} + +int kal_net_peer(kal_net_conn c, kal_endpoint* out) { + return report_address(okl::nr_getpeername, fd_of(c), out); +} + +int kal_net_local(kal_net_conn c, kal_endpoint* out) { + return report_address(okl::nr_getsockname, fd_of(c), out); +} + +int kal_net_listener_local(kal_net_listener l, kal_endpoint* out) { + return report_address(okl::nr_getsockname, fd_of(l), out); +} + +int kal_net_shutdown(kal_net_conn c, int direction) { + const int fd = fd_of(c); + if (fd < 0) return kal_err_invalid; + + // The kernel numbers the directions from zero and this interface from one, + // so the mapping is written out rather than arithmetic upon the argument. A + // direction this interface does not define is refused rather than passed + // through, because the kernel would read an unknown number as SHUT_RD. + okl_long how; + switch (direction) { + case KAL_SHUT_READ: how = 0; break; + case KAL_SHUT_WRITE: how = 1; break; + case KAL_SHUT_BOTH: how = 2; break; + default: return kal_err_invalid; + } + + const okl_long r = okl::sys(okl::nr_shutdown, fd, how); + if (okl::failed(r)) return okl::translate(r); + return kal_ok; +} + +void kal_net_close(kal_net_conn c) { + const int fd = fd_of(c); + if (fd < 0) return; + okl::sys(okl::nr_close, fd); + okl::retire(c.h); +} + +void kal_net_close_listener(kal_net_listener l) { + const int fd = fd_of(l); + if (fd < 0) return; + okl::sys(okl::nr_close, fd); + okl::retire(l.h); +} + +// Both positions hold on this kernel. IPv6 is configurable out of a build, but +// the socket call then reports it at the point of the attempt, and a word that +// claimed less than the kernel offers would withhold a facility a caller could +// have used. +const kal_uintptr kal_net_props = KAL_NET_PROP_IPV6 | KAL_NET_PROP_HALFCLOSE; + +} // extern "C" diff --git a/src/process.cpp b/src/process.cpp index aeb2b73..d3e4c2b 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -101,6 +101,122 @@ int kal_process_spawn(kal_dir base, return kal_ok; } +// A channel: a pair of streams of which one end is meant to cross a spawn. +// +// WHY THIS IS A KERNEL FACILITY AND kal::kit's CHANNEL IS NOT. A started program +// is another address space, so a pointer into this one is not something it can +// be handed. The pair must therefore be made of whatever the environment carries +// across a spawn, which here is a descriptor. +// +// BOTH ENDS ARE OWNED AND BOTH ARE RELEASED THROUGH kal_process_channel_close. +// A parent that does not release the far end after the spawn never observes the +// end of input on its own --- the classic deadlock of this arrangement, and the +// reason the release is declared beside the operation rather than left to +// openkal.stream, which has no release at all. +int kal_process_channel(kal_stream* mine, kal_stream* theirs) { + if (mine == nullptr || theirs == nullptr) return kal_err_invalid; + + int fds[2] = { -1, -1 }; + // O_CLOEXEC on both. The far end is placed deliberately, by the spawn that + // receives it; an end that leaked into every other started program would + // keep the channel open after the intended reader had closed it, and the + // writer would then never see the end of input. + const okl_long r = okl::sys(okl::nr_pipe2, reinterpret_cast(fds), + okl::o_cloexec); + if (okl::failed(r)) return okl::translate(r); + + // THE STREAMS ARE BARE DESCRIPTORS AND NOT PACKED HANDLES, because + // openkal.stream's transfer operations take what the environment takes. + // kal_fs_stream reports a file's stream the same way and for the same + // reason. + *mine = kal_stream{ static_cast(fds[0]) }; // the reading end + *theirs = kal_stream{ static_cast(fds[1]) }; // the writing end + return kal_ok; +} + +void kal_process_channel_close(kal_stream s) { + // A bare descriptor, so there is no generation to retire. The standard + // streams are borrowed and are numbered 0, 1 and 2; closing one of those + // through this operation would take a stream away from the whole program, + // so they are refused rather than closed. + const okl_long fd = static_cast(s.h); + if (fd < 3) return; + okl::sys(okl::nr_close, fd); +} + +// Starting a program that receives exactly the directories named. +// +// THE GRANTS ARE PLACED AS DESCRIPTORS THREE AND UPWARD, which is the +// arrangement kal_fs_preopen reads them back from. The inverse relationship +// clause 7.11 describes is therefore between this operation and that one, and +// it is why the two must agree about the numbering rather than each choosing. +// +// A COUNT OF ZERO IS NOT THE SAME AS kal_process_spawn. It starts a program with +// no preopens at all, which is the whole reason a caller reaches for this +// operation, so the loop below is not skipped when there is nothing to place --- +// what matters is that nothing else is inherited either. +int kal_process_spawn_with(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 kal_spawn_streams* streams, + const kal_preopen* grants, kal_uintptr grant_count, + kal_process* out) { + const int b = okl::unpack(base.h); + if (b < 0 || out == nullptr) return kal_err_invalid; + if (!okl::acceptable(path, path_len)) return kal_err_invalid; + if (grant_count > 0 && grants == nullptr) return kal_err_invalid; + okl::terminated p(path, path_len); + if (!p.ok) return kal_err_invalid; + + vector args, envs; + if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; + if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; + + // Resolved before the fork, because a failure after it would leave a child + // to be reaped and a caller with an error it cannot act upon. + constexpr kal_uintptr max_grants = 16; + if (grant_count > max_grants) return kal_err_invalid; + int granted[max_grants]; + for (kal_uintptr i = 0; i < grant_count; ++i) { + granted[i] = okl::unpack(grants[i].dir.h); + if (granted[i] < 0) return kal_err_invalid; + } + + const okl_long in = streams ? static_cast(streams->in) : 0; + const okl_long ou = streams ? static_cast(streams->out) : 0; + const okl_long er = streams ? static_cast(streams->err) : 0; + + const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0); + if (okl::failed(child)) return okl::translate(child); + + if (child == 0) { + if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0); + if (ou != 0) okl::sys(okl::nr_dup3, ou, 1, 0); + if (er != 0) okl::sys(okl::nr_dup3, er, 2, 0); + + // ⚠️ dup3 REFUSES A DUPLICATION ONTO ITSELF, which the ordinary case + // reaches whenever a granted directory already occupies the number it + // is destined for. Refusing there is correct of dup3 --- the flags could + // not be applied --- and here it means the descriptor is already in + // place, so it is left alone rather than treated as a failure. + for (kal_uintptr i = 0; i < grant_count; ++i) { + const okl_long want = static_cast(3 + i); + if (granted[i] != want) + okl::sys(okl::nr_dup3, granted[i], want, 0); + } + + okl::sys(okl::nr_execveat, b, reinterpret_cast(p.buf), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots), 0); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + } + + *out = kal_process{ static_cast(child) }; + return kal_ok; +} + int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) { if (h.h == 0) return kal_err_invalid; int st = 0; @@ -137,6 +253,7 @@ void kal_process_close(kal_process) { } const kal_uintptr kal_process_props = KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING - | KAL_PROCESS_PROP_EXIT_STATUS; + | KAL_PROCESS_PROP_EXIT_STATUS + | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; } diff --git a/src/space.cpp b/src/space.cpp new file mode 100644 index 0000000..0b5bf5b --- /dev/null +++ b/src/space.cpp @@ -0,0 +1,67 @@ +#include "sys.h" +#include + +// openkal.space upon clone(2). +// +// THE KERNEL'S PRIMITIVE IS THE INTERFACE'S OPERATION, WHICH IS WHY THE +// INTERFACE HAS ONE. clone with SIGCHLD and no CLONE_VM copies the address space +// and begins execution in the copy; there is no form that does the first without +// the second. An interface separating them would have obliged this file to park +// the started context and build a channel by which to tell it what to run, which +// clause 7.1 excludes. The specification was changed rather than this file. + +extern "C" { + +int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, + kal_process* out) { + if (entry == nullptr || out == nullptr) return kal_err_invalid; + + // THE STACK ARGUMENT IS NOT PASSED TO THE KERNEL, AND THE HEADER SAYS SO. + // + // Passing a stack to clone requires CLONE_VM --- the child would otherwise + // be running on a copy of the caller's stack at an address the caller chose, + // and the kernel's own copy of the stack is already correct. CLONE_VM is the + // opposite of what this interface provides: it would share the address space + // rather than copy it, which is openkal.task. + // + // So the copied stack is used, the argument is accepted and ignored, and the + // header states that an implementation whose environment gives the started + // context a stack of its own does exactly this. A caller cannot observe + // which occurred and has no decision resting upon it. + (void)stack_top; + + const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0); + if (okl::failed(child)) return okl::translate(child); + + if (child == 0) { + entry(arg); + // THE ENTRY IS NOT REQUIRED TO RETURN, AND IF IT DOES THE CONTEXT ENDS. + // + // Returning from here would return into clone's caller in the copied + // space, which is the whole address space of the program running a + // second time from the middle of this function. Ending the context is + // the only defined thing to do, and the status says the entry returned + // rather than choosing one. + okl::sys(okl::nr_exit_group, 0); + for (;;) { } + } + + *out = kal_process{ static_cast(child) }; + return kal_ok; +} + +// Both positions hold on this kernel. +// +// The handles accompany the memory: clone without CLONE_FILES gives the started +// context a copy of the descriptor table, so every handle the caller holds is +// open in the copy at the same number, and the packing in handle.h recovers the +// same descriptor from the same word. +// +// The copy is deferred: Linux maps the pages copy-on-write, so a store to copied +// memory can fail with the machine out of memory after this call has already +// reported success. An implementation cannot undefer that, and stating it is +// what lets a program that cannot tolerate it know which environment it is in. +const kal_uintptr kal_space_props = + KAL_SPACE_PROP_CLONE_HANDLES | KAL_SPACE_PROP_DEFERRED_COPY; + +} // extern "C" diff --git a/src/sys.h b/src/sys.h index 82975e3..1ab9e0a 100644 --- a/src/sys.h +++ b/src/sys.h @@ -98,6 +98,15 @@ enum : okl_long { nr_renameat = 264, nr_readlinkat = 267, nr_dup3 = 292, nr_execveat = 322, nr_dup2 = 33, nr_utimensat = 280, nr_getrandom = 318, + // openkal.net and openkal.datagram + nr_socket = 41, nr_connect = 42, nr_accept = 43, nr_sendto = 44, + nr_recvfrom = 45, nr_shutdown = 48, nr_bind = 49, nr_listen = 50, + nr_getsockname = 51, nr_getpeername = 52, nr_accept4 = 288, + nr_setsockopt = 54, nr_pipe2 = 293, + // openkal.timeout. ppoll and not poll: the bound is stated in nanoseconds + // and poll takes milliseconds, so poll could not express a bound finer than + // the granularity this implementation reports. + nr_ppoll = 271, }; #elif defined(__aarch64__) @@ -172,6 +181,14 @@ enum : okl_long { nr_dup3 = 24, nr_execveat = 281, nr_dup2 = -1, nr_arch_prctl = -1, nr_utimensat = 88, nr_getrandom = 278, + // openkal.net and openkal.datagram + nr_socket = 198, nr_connect = 203, nr_accept = 202, nr_sendto = 206, + nr_recvfrom = 207, nr_shutdown = 210, nr_bind = 200, nr_listen = 201, + nr_getsockname = 204, nr_getpeername = 205, nr_accept4 = 242, + nr_setsockopt = 208, nr_pipe2 = 59, + // openkal.timeout. This architecture has no `poll' at all, only `ppoll', + // which is a second reason the bound is expressed through the latter. + nr_ppoll = 73, }; #else @@ -290,6 +307,79 @@ struct kdirent64 { enum : unsigned char { dt_dir = 4, dt_reg = 8, dt_lnk = 10 }; +// --- openkal.terminal ------------------------------------------------------ +// +// The kernel's terminal settings, in the kernel's own layout. A C library's +// `struct termios' is not this structure: several of them carry additional +// fields, and one compiled against a different library would read the wrong +// words. The comment at the head of this file states the rule; this is an +// instance of it. +struct ktermios { + okl_u32 iflag; + okl_u32 oflag; + okl_u32 cflag; + okl_u32 lflag; + unsigned char line; + unsigned char cc[19]; +}; + +struct kwinsize { + unsigned short row; + unsigned short col; + unsigned short xpixel; + unsigned short ypixel; +}; + +enum : okl_long { + tcgets = 0x5401, tcsets = 0x5402, tiocgwinsz = 0x5413, +}; + +// Positions within ktermios::lflag. Named here for the same reason the numbers +// above are: they belong to the kernel and not to any library. +enum : okl_u32 { t_icanon = 0000002u, t_echo = 0000010u }; + +// --- openkal.net and openkal.datagram -------------------------------------- +// +// The kernel's socket address structures. `ksockaddr_storage' is large enough +// for either family and is what a call that reports an address is given, so +// that a reply naming a family this implementation did not ask for cannot write +// beyond the object. +enum : okl_long { + af_inet = 2, af_inet6 = 10, + sock_stream = 1, sock_dgram = 2, sock_cloexec = 02000000, + ipproto_tcp = 6, ipproto_udp = 17, + sol_socket = 1, so_reuseaddr = 2, +}; + +struct ksockaddr_in { + unsigned short family; + unsigned short port; // network order + okl_u32 addr; // network order + unsigned char zero[8]; +}; + +struct ksockaddr_in6 { + unsigned short family; + unsigned short port; // network order + okl_u32 flowinfo; + unsigned char addr[16]; // network order + okl_u32 scope_id; +}; + +struct ksockaddr_storage { + unsigned short family; + unsigned char pad[126]; +}; + +// --- openkal.timeout ------------------------------------------------------- +struct kpollfd { + int fd; + short events; + short revents; +}; + +enum : short { poll_in = 0x0001, poll_out = 0x0004 }; + // --- operations used by more than one interface ---------------------------- inline okl_long write_all(int fd, const void* p, okl_uptr n) { diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..887caf9 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,119 @@ +#include "sys.h" +#include + +// openkal.terminal upon the kernel's terminal ioctls. +// +// EVERY OPERATION IS CONDITIONED ON THE STREAM BEING ONE. TCGETS succeeds for a +// terminal and reports ENOTTY otherwise, which is the same test kal_stream_props +// performs and the same one every C library performs. Performing it here is what +// lets the interface report a refusal for a file rather than acting upon it. + +namespace { + +// The mode word this interface defines, read out of the kernel's local flags. +// +// KAL_TERM_LINE_EDIT IS ICANON AND NOT ICANON PLUS ECHO. The two are separate +// positions in this interface because a program that wants a password prompt +// turns off one and leaves the other, and a backend that conflated them would +// make that program's intent inexpressible. +kal_uintptr mode_of(const okl::ktermios& t) { + kal_uintptr m = 0; + if ((t.lflag & okl::t_icanon) != 0) m |= KAL_TERM_LINE_EDIT; + if ((t.lflag & okl::t_echo) != 0) m |= KAL_TERM_ECHO; + return m; +} + +int get_termios(kal_stream s, okl::ktermios& out) { + const okl_long r = okl::sys(okl::nr_ioctl, static_cast(s.h), + okl::tcgets, reinterpret_cast(&out)); + if (okl::failed(r)) { + // ENOTTY is the answer for a stream that is not a terminal, and this + // interface states that answer as kal_err_not_supported rather than + // passing the kernel's own classification through. + if (r == -okl::e_notty || r == -okl::e_inval) return kal_err_not_supported; + return okl::translate(r); + } + return kal_ok; +} + +} // namespace + +extern "C" { + +int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { + if (mode == nullptr) return kal_err_invalid; + okl::ktermios t{}; + const int rc = get_termios(s, t); + if (rc != kal_ok) return rc; + *mode = mode_of(t); + return kal_ok; +} + +int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { + // READ, MODIFY, WRITE, AND NOT WRITE ALONE. The kernel's structure carries + // input flags, output flags, a baud rate and the control characters, none of + // which this interface names. Writing a structure this implementation + // composed from the mode word alone would silently discard all of them --- + // the terminal a program returned to would have a different baud rate than + // the one it found. + okl::ktermios t{}; + const int rc = get_termios(s, t); + if (rc != kal_ok) return rc; + + if ((mode & KAL_TERM_LINE_EDIT) != 0) t.lflag |= okl::t_icanon; + else t.lflag &= ~okl::t_icanon; + if ((mode & KAL_TERM_ECHO) != 0) t.lflag |= okl::t_echo; + else t.lflag &= ~okl::t_echo; + + // A position this implementation does not distinguish is ignored rather + // than refused, which is what clause 6.2 requires of a word: a program + // compiled against a later revision sets a position this build has never + // heard of, and refusing would make that program fail against an + // implementation that is behaving correctly. + const okl_long w = okl::sys(okl::nr_ioctl, static_cast(s.h), + okl::tcsets, reinterpret_cast(&t)); + if (okl::failed(w)) { + if (w == -okl::e_notty || w == -okl::e_inval) return kal_err_not_supported; + return okl::translate(w); + } + return kal_ok; +} + +int kal_terminal_size(kal_stream s, kal_uintptr* cols, kal_uintptr* rows) { + if (cols == nullptr || rows == nullptr) return kal_err_invalid; + okl::kwinsize w{}; + const okl_long r = okl::sys(okl::nr_ioctl, static_cast(s.h), + okl::tiocgwinsz, reinterpret_cast(&w)); + if (okl::failed(r)) { + // BOTH OUTPUTS ARE LEFT UNTOUCHED, which the interface requires. A + // serial line answers ENOTTY here while answering TCGETS, so this is not + // the same condition as "not a terminal" and the outputs must survive + // it for a caller to distinguish them. + if (r == -okl::e_notty || r == -okl::e_inval) return kal_err_not_supported; + return okl::translate(r); + } + *cols = static_cast(w.col); + *rows = static_cast(w.row); + return kal_ok; +} + +kal_uintptr kal_terminal_props(kal_stream s) { + kal_uintptr p = 0; + + okl::ktermios t{}; + if (get_termios(s, t) == kal_ok) p |= KAL_TERM_PROP_MODE; + + // THE SIZE POSITION IS ASKED FOR RATHER THAN ASSUMED FROM THE FIRST. A + // pseudo terminal answers both; a serial line answers TCGETS and not + // TIOCGWINSZ. Deriving one from the other would make the word claim a + // facility the very next call refuses, which is the disagreement clause 6.2 + // exists to prevent. + okl::kwinsize w{}; + const okl_long r = okl::sys(okl::nr_ioctl, static_cast(s.h), + okl::tiocgwinsz, reinterpret_cast(&w)); + if (!okl::failed(r)) p |= KAL_TERM_PROP_SIZE; + + return p; +} + +} // extern "C" diff --git a/src/timeout.cpp b/src/timeout.cpp new file mode 100644 index 0000000..a6f336d --- /dev/null +++ b/src/timeout.cpp @@ -0,0 +1,153 @@ +#include "sys.h" +#include "handle.h" +#include "endpoint.h" +#include + +// openkal.timeout upon ppoll(2) and wait4(2). +// +// THE BOUND IS APPLIED BEFORE THE OPERATION AND NOT DURING IT. ppoll reports +// whether a descriptor would transfer without blocking, so a bounded read is a +// bounded wait for readiness followed by the ordinary read. This is what the +// environment already does at the point of the call, which is why clause 6.3 +// records readiness notification as the alternative that was NOT adopted: an +// interface reporting readiness would oblige an implementation to maintain a set +// and a context of its own, and this one obliges it to maintain nothing. +// +// The bound is therefore upon the WAIT and not upon the transfer. A read that +// becomes ready within the bound and then transfers slowly is not interrupted, +// which is the behaviour every environment's own bounded read has. + +namespace { + +// A duration of zero denotes no bound, which is the convention kal_task_wait +// establishes. ppoll expresses that by being given no timespec at all. +const okl::ktimespec* bound_of(kal_u64 ns, okl::ktimespec& storage) { + if (ns == 0) return nullptr; + storage.sec = static_cast(ns / 1000000000ull); + storage.nsec = static_cast(ns % 1000000000ull); + return &storage; +} + +// Waits for one descriptor. Reports kal_ok when it is ready, kal_err_again when +// the bound expired, and a translated error otherwise. +int await(int fd, short events, kal_u64 ns) { + okl::kpollfd p{ fd, events, 0 }; + okl::ktimespec ts{}; + const okl::ktimespec* to = bound_of(ns, ts); + + for (;;) { + const okl_long r = okl::sys(okl::nr_ppoll, reinterpret_cast(&p), + 1, reinterpret_cast(to), 0, 0); + // AN INTERRUPTED WAIT IS NOT RETRIED WITH THE WHOLE BOUND AGAIN. + // + // Retrying with the original duration would make the bound restart at + // every signal, so a program on a system that delivers them regularly + // would wait without end while appearing to be bounded. ppoll leaves the + // caller's timespec untouched, so there is nothing to resume from, and + // the honest report is that the operation did not complete. + if (okl::interrupted(r)) return kal_err_again; + if (okl::failed(r)) return okl::translate(r); + if (r == 0) return kal_err_again; // the bound expired + return kal_ok; + } +} + +} // namespace + +extern "C" { + +kal_io_result kal_timeout_read(kal_stream s, void* buf, kal_uintptr len, kal_u64 ns) { + // A transfer of zero bytes does not wait and is not bounded. Waiting first + // would turn a call that always succeeds into one that can expire. + if (len == 0) return { 0, kal_ok }; + + const int fd = okl::unpack(s.h); + // THE STANDARD STREAMS ARE NOT PACKED HANDLES. openkal.stream reports them + // as the descriptors themselves, so a word that does not unpack is taken to + // be one of those rather than being refused. + const int use = (fd >= 0) ? fd : static_cast(s.h); + + if (const int rc = await(use, okl::poll_in, ns); rc != kal_ok) return { 0, rc }; + return kal_stream_read(s, buf, len); +} + +kal_io_result kal_timeout_write(kal_stream s, const void* buf, kal_uintptr len, kal_u64 ns) { + if (len == 0) return { 0, kal_ok }; + + const int fd = okl::unpack(s.h); + const int use = (fd >= 0) ? fd : static_cast(s.h); + + if (const int rc = await(use, okl::poll_out, ns); rc != kal_ok) return { 0, rc }; + return kal_stream_write(s, buf, len); +} + +int kal_timeout_accept(kal_net_listener l, kal_u64 ns, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + const int fd = okl::unpack(l.h); + if (fd < 0) return kal_err_invalid; + + if (const int rc = await(fd, okl::poll_in, ns); rc != kal_ok) return rc; + return kal_net_accept(l, out); +} + +kal_io_result kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, + kal_endpoint* from, kal_u64 ns) { + const int fd = okl::unpack(d.h); + if (fd < 0) return { 0, kal_err_invalid }; + + if (const int rc = await(fd, okl::poll_in, ns); rc != kal_ok) return { 0, rc }; + return kal_datagram_recv_from(d, buf, len, from); +} + +int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* terminated) { + if (p.h == 0) return kal_err_invalid; + + // WNOHANG AND A POLLING LOOP, BECAUSE THE KERNEL HAS NO BOUNDED WAIT FOR A + // CHILD. wait4 either blocks or does not wait at all; there is no timespec. + // + // The alternative is a signal handler for SIGCHLD, which this implementation + // does not have and would not want: a handler is process-wide state, and an + // implementation that installed one would take a facility away from the + // program above it. The loop is what the environment permits, and the + // interval is bounded below by the granularity this interface reports so + // that the polling cost is stated rather than hidden. + constexpr okl_u64 interval_ns = 1000000ull; // one millisecond + okl_u64 waited = 0; + + for (;;) { + int st = 0; + const okl_long r = okl::sys(okl::nr_wait4, static_cast(p.h), + reinterpret_cast(&st), + 1 /* WNOHANG */, 0); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return okl::translate(r); + + if (r != 0) { + const int signalled = st & 0x7f; + if (signalled == 0) { + if (status) *status = (st >> 8) & 0xff; + if (terminated) *terminated = 0; + } else { + if (status) *status = signalled; + if (terminated) *terminated = 1; + } + return kal_ok; + } + + if (ns != 0 && waited >= ns) return kal_err_again; + + okl::ktimespec ts{ 0, static_cast(interval_ns) }; + okl::sys(okl::nr_nanosleep, reinterpret_cast(&ts), 0); + waited += interval_ns; + } +} + +// The kernel states a bound in nanoseconds and the clock advances at the +// scheduler's resolution, so a bound finer than a tick is rounded up by the +// scheduler rather than refused. One millisecond is reported because it is the +// interval the child-waiting loop above polls at, and a caller asking for less +// than the coarsest of the operations here would otherwise be told a number one +// of them cannot meet. +const kal_uintptr kal_timeout_granularity_ns = 1000000u; + +} // extern "C" diff --git a/tests/conformance_v08.cpp b/tests/conformance_v08.cpp new file mode 100644 index 0000000..dae0ea8 --- /dev/null +++ b/tests/conformance_v08.cpp @@ -0,0 +1,270 @@ +// The five interfaces version 0.8 added, examined through the module form. +// +// THIS IS THE C++ HALF OF CLAUSE 4.3. The specification distributes its +// declarations in two forms and requires the two to declare the same entities. +// tools/check-declarations.sh in the specification repository examines the C +// form against SURFACE.txt; the C++ form is examined here, where a build of the +// modules already exists. Neither form is therefore the other's source. +// +// The observations are of behaviour and not only of existence. A test that named +// each entity and did nothing with it would compile against an implementation +// whose every operation returned an error, and would report that as conformance. +#include +#include +import openkal.types; +import openkal.stream; +import openkal.terminal; +import openkal.net; +import openkal.datagram; +import openkal.space; +import openkal.timeout; +import openkal.process; +import openkal.abort; + +namespace { + +int failures = 0; + +void check(bool held, const char* what) { + if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +} + +kal_endpoint loopback(kal_u32 port) { + kal_endpoint ep{}; + ep.addr[0] = 127; ep.addr[3] = 1; + ep.addr_len = 4; + ep.port = port; + return ep; +} + +// openkal.terminal +// +// A run under a pipe has no terminal, which is the ordinary case here. What can +// be observed without one is the refusal, and the refusal is the half of the +// contract that says the interface reports rather than acts upon a stream it +// does not apply to. +void terminal_section() { + const auto out = kal_stdout(); + // The property is named through the module rather than through the macro: + // a macro does not cross a module boundary, and kal::stream is where the + // module form states it. + const bool interactive = + kal::stream_props{kal_stream_props(out)}.has(kal::stream_prop::interactive); + + const auto m = kal::terminal::get_mode(out); + if (interactive) { + check(m.e == kal_ok, "an interactive stream reports its mode"); + check(kal::terminal::set_mode(out, m.m) == kal_ok, + "the mode that was read can be set back"); + } else { + check(m.e == kal_err_not_supported, + "a stream that is not interactive refuses get_mode"); + } + + // The size outputs survive a refusal. They are pre-set to values the + // operation would not produce, so a backend that wrote them before failing + // would be visible here rather than in a caller's arithmetic. + kal_uintptr cols = 0xDEAD, rows = 0xBEEF; + const int rc = kal_terminal_size(out, &cols, &rows); + if (rc != kal_ok) + check(cols == 0xDEAD && rows == 0xBEEF, + "a refused size leaves both outputs untouched"); +} + +// openkal.net +void net_section() { + const auto want = loopback(0); + const auto l = kal::net::listen(want); + check(l.e == kal_ok, "a listener opens on the loopback address"); + if (l.e != kal_ok) return; + + const auto bound = kal::net::local(l.l); + check(bound.e == kal_ok && bound.ep.port != 0, + "a listener opened on port zero reports the port it was given"); + if (bound.e != kal_ok || bound.ep.port == 0) { kal::net::close(l.l); return; } + + const auto c = kal::net::connect(loopback(bound.ep.port)); + check(c.e == kal_ok, "a connection to the listener is established"); + if (c.e != kal_ok) { kal::net::close(l.l); return; } + + const auto a = kal::net::accept(l.l); + check(a.e == kal_ok, "the listener accepts the connection"); + if (a.e != kal_ok) { kal::net::close(c.c); kal::net::close(l.l); return; } + + // The streams the two connections own. Borrowed, and released with the + // connection rather than separately. + const auto cs = kal::net::stream(c.c); + const auto ss = kal::net::stream(a.c); + + // A connection is a stream, and its bytes move through the stream + // operations. That this interface adds no transfer operation of its own is + // the property being observed. + const char msg[] = "openkal"; + const auto w = kal_stream_write(cs, msg, sizeof msg - 1); + check(w.e == kal_ok && w.n == sizeof msg - 1, + "a connection carries bytes through the stream operations"); + + char buf[16] = {}; + const auto r = kal_stream_read(ss, buf, sizeof buf); + check(r.e == kal_ok && r.n == sizeof msg - 1 && + std::memcmp(buf, msg, sizeof msg - 1) == 0, + "the bytes read are the bytes written"); + + if (kal::net::has(kal::net::halfclose)) { + check(kal::net::shutdown(c.c, kal::net::shut::write) == kal_ok, + "a claimed half-closure is performed"); + char eof[4] = {}; + const auto e = kal_stream_read(ss, eof, sizeof eof); + check(e.e == kal_ok && e.n == 0, + "the peer observes end of input after a half-closure"); + } + + // An endpoint whose length this implementation does not know is refused + // rather than read as one it does. + kal_endpoint odd{}; + odd.addr_len = 7; + const auto bad = kal::net::connect(odd); + check(bad.e == kal_err_invalid, + "an endpoint of unknown length is refused, not misread"); + + kal::net::close(a.c); + kal::net::close(c.c); + kal::net::close(l.l); +} + +// openkal.datagram +void datagram_section() { + const auto rx = kal::datagram::open(loopback(0)); + check(rx.e == kal_ok, "a datagram endpoint opens on the loopback address"); + if (rx.e != kal_ok) return; + + const auto bound = kal::datagram::local(rx.d); + check(bound.e == kal_ok && bound.ep.port != 0, + "an endpoint opened on port zero reports the port it was given"); + if (bound.e != kal_ok || bound.ep.port == 0) { kal::datagram::close(rx.d); return; } + + const auto tx = kal::datagram::open(); + check(tx.e == kal_ok, "an endpoint that only sends opens without an address"); + if (tx.e != kal_ok) { kal::datagram::close(rx.d); return; } + + const char msg[] = "openkal"; + const auto w = kal::datagram::send_to(tx.d, msg, sizeof msg - 1, + loopback(bound.ep.port)); + check(w.e == kal_ok && w.n == sizeof msg - 1, + "a message is sent whole and the count is the length given"); + + char buf[16] = {}; + const auto got = kal::datagram::recv_from(rx.d, buf, sizeof buf); + check(got.r.e == kal_ok && got.r.n == sizeof msg - 1 && + std::memcmp(buf, msg, sizeof msg - 1) == 0, + "the message received is the message sent"); + check(got.from.addr_len == 4, "the sender of a received message is reported"); + + kal::datagram::close(tx.d); + kal::datagram::close(rx.d); +} + +// openkal.space +// +// The exit status is the only channel a separate address space has, so the entry +// reports through it and the caller's own memory is checked to be unchanged. +int marker = 0; + +void child_entry(void* arg) { + marker = 1; // in the copy, not in the caller + kal_exit(arg == nullptr ? 3 : 7); +} + +void space_section() { + int local = 0; + const auto p = kal::space::start(&child_entry, static_cast(&local), + nullptr); + check(p.e == kal_ok, "a context starts in a copy of the space"); + if (p.e != kal_ok) return; + + int status = 0, terminated = 0; + check(kal_process_wait(p.p, &status, &terminated) == kal_ok, + "the started context is waited for as a process"); + check(terminated == 0, "the started context ended of its own accord"); + check(status == 7, "the entry received the argument it was given"); + check(marker == 0, "a store in the copied space is not observed in the original"); + kal_process_close(p.p); +} + +// openkal.timeout +void timeout_section() { + check(kal_timeout_granularity_ns > 0, + "the granularity is a positive number of nanoseconds"); + + // A bounded read of a listener that nobody connects to must expire rather + // than wait. A listener is used rather than the standard input because the + // latter may be a file, which is always ready. + const auto l = kal::net::listen(loopback(0)); + if (l.e == kal_ok) { + const auto a = kal::timeout::accept(l.l, 1000000 /* one millisecond */); + check(a.e == kal_err_again, + "an accept that nobody answers expires as kal_err_again"); + kal::net::close(l.l); + } + + // A transfer of zero bytes does not wait and is not bounded. + const auto w = kal::timeout::write(kal_stdout(), "", 0, 1); + check(w.e == kal_ok, "a bounded transfer of zero bytes succeeds"); +} + +// The three operations openkal 0.8 adds to openkal.process. +// +// ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, which is +// not true of adding a new interface: clause 6.1 makes a new one optional and +// clause 6.1 makes an incomplete one a deviation. The surface checker caught the +// omission here before anything else did, and these observations are what says +// the names do something rather than merely existing. +void process_additions_section() { + // A channel carries bytes from one end to the other. Both ends are owned and + // both are released through kal_process_channel_close. + kal_stream mine{}, theirs{}; + const int rc = kal_process_channel(&mine, &theirs); + check(rc == kal_ok, "a channel is created"); + if (rc != kal_ok) return; + + const char msg[] = "through the channel"; + const auto w = kal_stream_write(theirs, msg, sizeof msg - 1); + check(w.e == kal_ok && w.n == sizeof msg - 1, + "the far end of a channel accepts bytes"); + + char buf[64] = {}; + const auto r = kal_stream_read(mine, buf, sizeof buf); + check(r.e == kal_ok && r.n == sizeof msg - 1 && + std::memcmp(buf, msg, sizeof msg - 1) == 0, + "the near end reads what the far end wrote"); + + // THE END OF INPUT IS WHAT THE RELEASE IS FOR. A parent that does not close + // 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. + kal_process_channel_close(theirs); + const auto eof = kal_stream_read(mine, buf, sizeof buf); + check(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 claims both additions, so both must be answered. Named + // through the module, because a macro does not cross a module boundary. + check(kal::process::has(kal::process::channel), + "the property word claims the channel it just provided"); + check(kal::process::has(kal::process::grant_dir), + "the property word claims the directory grant"); +} + +} // namespace + +int main() { + process_additions_section(); + terminal_section(); + net_section(); + datagram_section(); + space_section(); + timeout_section(); + + if (failures == 0) std::printf("openkal 0.8 interfaces: every observation held\n"); + return failures == 0 ? 0 : 1; +}