Skip to content

Commit c096389

Browse files
Implement the five interfaces openkal 0.8 adds (#15)
* implement the five interfaces openkal 0.8 adds openkal.terminal upon the kernel's terminal ioctls, openkal.net and openkal.datagram upon its socket calls, openkal.space upon clone, and openkal.timeout upon ppoll and a bounded wait for a child. Each is written on the kernel's own calling convention and names no facility a program above might also define, which is the property this implementation has had since version 0.5 and which CI asserts by examining undefined symbols. ## What the implementation established about the interface openkal.space is one operation because the kernel's primitive is one act. clone copies the address space and begins execution in the copy; there is no form that does the first without the second. The specification was changed rather than this file, because an implementation asked to separate them would have to park a started context and build a channel to tell it what to run. openkal.net yields an owned connection and a borrowed stream, for the same reason kal_file and kal_fs_stream are separate: the owned handle carries a generation so that a released one stops being valid, and a stream handle is the bare descriptor because that is what the transfer operations take. ## Details worth stating The terminal mode is read, modified and written rather than written alone. The kernel's structure carries input flags, output flags, a baud rate and the control characters, none of which this interface names; composing one from the mode word would discard all of them silently. kal_terminal_props asks TIOCGWINSZ rather than deriving the answer from TCGETS. A pseudo terminal answers both and a serial line answers only the first, so a word derived from one would claim a facility the next call refuses. The endpoint conversion is shared between net and datagram because the type is, and either interface may be provided without the other. An interrupted bounded wait is reported as an expiry rather than retried with the whole bound again, which would make the bound restart at every signal and leave a program waiting without end while appearing to be bounded. kal_timeout_wait_process polls with WNOHANG because the kernel has no bounded wait for a child. The alternative is a SIGCHLD handler, which is process-wide state an implementation would be taking away from the program above it. tests/conformance_v08.cpp the C++ half of clause 4.3 for the new interfaces mcpp test 7 passed, 0 failed conformance suite 100 observations held, 0 did not hold terminal's interactive path observed under a pty * Provide the three operations openkal 0.8 adds to openkal.process ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, and adding a new interface obliges none. Clause 6.1 makes an interface an implementation does not provide absent at the link and not a deviation; it makes one provided IN PART a deviation. The five interfaces version 0.8 adds are therefore free for a backend to decline, and the three names added to openkal.process are not. The specification's own surface checker is what said so, before anything else noticed: openkal.process is provided in part: 3 of 8 names are not exported -- kal_process_channel kal_process_channel_close kal_process_spawn_with kal_process_channel is pipe2 with O_CLOEXEC on both ends. 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 never see the end of input. The streams are bare descriptors rather than 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. kal_process_channel_close refuses descriptors below three. They are the standard streams, which are borrowed; closing one through this operation would take a stream away from the whole program. kal_process_spawn_with places the grants as descriptors three and upward, which is where kal_fs_preopen reads them back from. The inverse relationship clause 7.11 describes is between those two operations, which is why they must agree about the numbering rather than each choosing one. ⚠️ dup3 refuses a duplication onto itself, and the ordinary case reaches that whenever a granted directory already occupies the number it is destined for. Refusing 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. The grants are resolved before the fork. A failure after it would leave a child to be reaped and a caller holding an error it cannot act upon. surface exported surface is complete and conforms: 90 names mcpp test 7 passed, 0 failed observed a channel carries bytes, and closing the far end is observed as end of input on the near one * ci: assert the committed manifest names no local directory 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, where it was published and had to be reverted. The working tree here has carried the same rewrite more than once since, and only an audit before committing kept it out. The step runs first, so what it examines is what the commit contains rather than what the job has since done to it. openkal-musl carries the same check for the same reason. * Name the released specification and the released engine openkal 0.8.0 is published, so the manifest names it by version rather than by branch. A branch is how a change spanning these repositories is developed and is not a form a published package may carry: a consumer resolving from the index would be handed a reference that moves. The engine pin moves to mcpp 2026.8.27.1 and the repository variable MCPP_SOURCE_REF is cleared, so what this repository tests is the engine a user installs. --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent e556d34 commit c096389

12 files changed

Lines changed: 1302 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ on:
3030
default: ""
3131
env:
3232
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
33-
MCPP_VERSION: 2026.8.26.2
33+
MCPP_VERSION: 2026.8.27.1
3434
XLINGS_VERSION: v2026.8.17.2
3535
XLINGS_NON_INTERACTIVE: '1'
3636

@@ -49,6 +49,31 @@ jobs:
4949
# The specification is checked out at the branch under test where it has
5050
# one, so that this run asserts what it is for: that the specification as
5151
# written there and this implementation as written here agree today.
52+
# THE COMMITTED MANIFEST NAMES NO DIRECTORY OF ANYBODY'S MACHINE.
53+
#
54+
# Two scripts in the specification's repository rewrite this manifest to
55+
# name a working tree --- run-conformance.sh and run-kit-tests.sh --- and
56+
# both restore it through a trap. A trap does not fire when the process is
57+
# killed, and a run by hand followed by `git add -A` then publishes a path
58+
# that exists on one machine: a consumer resolving from the index is handed
59+
# a manifest pointing at a directory that exists nowhere.
60+
#
61+
# ⚠️ THAT HAS HAPPENED IN THIS ECOSYSTEM, in openkal-musl, and the working
62+
# tree here has carried the same rewrite more than once since. This step
63+
# runs first, so what it examines is what the commit contains.
64+
- name: The committed manifest names no local directory
65+
run: |
66+
set -euo pipefail
67+
bad=$(grep -nE '^[a-z-]+ = \{[^}]*path = "(/|[A-Za-z]:)' mcpp.toml || true)
68+
if [ -n "$bad" ]; then
69+
echo "::error::the committed manifest names an absolute path"
70+
printf '%s\n' "$bad" | sed 's/^/ /'
71+
echo " run 'git checkout -- mcpp.toml' after using the"
72+
echo " specification's conformance or kit scripts by hand."
73+
exit 1
74+
fi
75+
echo " ok every dependency is named by version, branch or a relative path"
76+
5277
- name: The specification
5378
run: |
5479
git clone --quiet https://github.com/mcpplibs/openkal.git .spec

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ compile_commands.json
1717
# What a system leaves behind.
1818
.DS_Store
1919
Thumbs.db
20+
21+
# The specification tree tools/run-conformance.sh clones beside the sources.
22+
.spec/

mcpp.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
namespace = "mcpplibs"
33
name = "openkal-linux"
4-
version = "0.5.4"
4+
version = "0.6.0"
55
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."
66
license = "Apache-2.0"
77

@@ -18,7 +18,7 @@ authors = ["mcpplibs"]
1818
repo = "https://github.com/mcpplibs/openkal-linux"
1919

2020
[dependencies]
21-
openkal = "0.7.0"
21+
openkal = "0.8.0"
2222

2323
# The package contributes definitions and no modules. The interface it
2424
# implements is declared by the specification package, which this package

src/datagram.cpp

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#include "sys.h"
2+
#include "handle.h"
3+
#include "endpoint.h"
4+
#include <openkal/datagram.h>
5+
6+
// openkal.datagram upon the kernel's socket calls.
7+
//
8+
// A DATAGRAM IS NOT PACKED AS A kal_stream, and the handle type is its own for
9+
// that reason: kal_stream_read reports a count and not a boundary, so reading a
10+
// datagram through it would lose the property that distinguishes this interface.
11+
// The packing is the same, the type is not, and the type is what prevents the
12+
// mistake.
13+
14+
namespace {
15+
16+
int fd_of(kal_datagram d) { return okl::unpack(d.h); }
17+
18+
} // namespace
19+
20+
extern "C" {
21+
22+
int kal_datagram_open(const kal_endpoint* local, kal_datagram* out) {
23+
if (out == nullptr) return kal_err_invalid;
24+
25+
// A null local endpoint asks for one that may send and whose receiving
26+
// address is unspecified. IPv4 is chosen for it, because a family must be
27+
// named at the point the socket is made and this is the one every
28+
// environment that has a network at all provides.
29+
okl_long family = okl::af_inet;
30+
if (local != nullptr) {
31+
family = okl::family_of(*local);
32+
if (family < 0) return kal_err_invalid;
33+
}
34+
35+
const okl_long fd = okl::sys(okl::nr_socket, family,
36+
okl::sock_dgram | okl::sock_cloexec,
37+
okl::ipproto_udp);
38+
if (okl::failed(fd)) return okl::translate(fd);
39+
40+
if (local != nullptr) {
41+
okl::ksockaddr_storage ss{};
42+
okl_long len = 0;
43+
if (const int rc = okl::to_kernel(*local, ss, len); rc != kal_ok) {
44+
okl::sys(okl::nr_close, fd);
45+
return rc;
46+
}
47+
if (const okl_long r = okl::sys(okl::nr_bind, fd,
48+
reinterpret_cast<okl_long>(&ss), len);
49+
okl::failed(r)) {
50+
okl::sys(okl::nr_close, fd);
51+
return okl::translate(r);
52+
}
53+
}
54+
55+
out->h = okl::pack(static_cast<int>(fd));
56+
if (out->h == 0) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; }
57+
return kal_ok;
58+
}
59+
60+
int kal_datagram_local(kal_datagram d, kal_endpoint* out) {
61+
if (out == nullptr) return kal_err_invalid;
62+
const int fd = fd_of(d);
63+
if (fd < 0) return kal_err_invalid;
64+
65+
okl::ksockaddr_storage ss{};
66+
okl_long len = static_cast<okl_long>(sizeof ss);
67+
const okl_long r = okl::sys(okl::nr_getsockname, fd,
68+
reinterpret_cast<okl_long>(&ss),
69+
reinterpret_cast<okl_long>(&len));
70+
if (okl::failed(r)) return okl::translate(r);
71+
return okl::from_kernel(ss, *out);
72+
}
73+
74+
kal_io_result kal_datagram_send_to(kal_datagram d, const void* buf, kal_uintptr len,
75+
const kal_endpoint* to) {
76+
const int fd = fd_of(d);
77+
if (fd < 0 || to == nullptr) return { 0, kal_err_invalid };
78+
79+
okl::ksockaddr_storage ss{};
80+
okl_long addrlen = 0;
81+
if (const int rc = okl::to_kernel(*to, ss, addrlen); rc != kal_ok)
82+
return { 0, rc };
83+
84+
for (;;) {
85+
const okl_long r = okl::sys(okl::nr_sendto, fd,
86+
reinterpret_cast<okl_long>(buf),
87+
static_cast<okl_long>(len), 0,
88+
reinterpret_cast<okl_long>(&ss), addrlen);
89+
if (okl::interrupted(r)) continue;
90+
if (okl::failed(r)) return { 0, okl::translate(r) };
91+
92+
// A MESSAGE IS SENT WHOLE OR NOT AT ALL, which is what this interface
93+
// states. The kernel reports a count anyway; a count short of the length
94+
// would mean the medium had split the message, which for a datagram
95+
// socket it does not do. Reporting the short count as success would give
96+
// a caller a partial send this interface says cannot occur, so it is
97+
// reported as a failure of the medium instead.
98+
const kal_uintptr n = static_cast<kal_uintptr>(r);
99+
return { n, n == len ? kal_ok : kal_err_io };
100+
}
101+
}
102+
103+
kal_io_result kal_datagram_recv_from(kal_datagram d, void* buf, kal_uintptr len,
104+
kal_endpoint* from) {
105+
const int fd = fd_of(d);
106+
if (fd < 0) return { 0, kal_err_invalid };
107+
108+
okl::ksockaddr_storage ss{};
109+
okl_long addrlen = static_cast<okl_long>(sizeof ss);
110+
111+
for (;;) {
112+
const okl_long r = okl::sys(okl::nr_recvfrom, fd,
113+
reinterpret_cast<okl_long>(buf),
114+
static_cast<okl_long>(len), 0,
115+
reinterpret_cast<okl_long>(&ss),
116+
reinterpret_cast<okl_long>(&addrlen));
117+
if (okl::interrupted(r)) continue;
118+
if (okl::failed(r)) return { 0, okl::translate(r) };
119+
120+
// THE COUNT REPORTED IS WHAT WAS PLACED IN THE BUFFER, not what was
121+
// sent. Without MSG_TRUNC the kernel already reports the former, which
122+
// is what this interface requires: a caller that trusted the larger
123+
// number would read beyond its own buffer.
124+
if (from != nullptr) {
125+
// A sender whose family this implementation does not know leaves the
126+
// endpoint zeroed rather than partly filled. The transfer still
127+
// happened and is reported; what is unknown is who sent it.
128+
if (okl::from_kernel(ss, *from) != kal_ok) {
129+
for (auto& b : from->addr) b = 0;
130+
from->addr_len = 0;
131+
from->port = 0;
132+
}
133+
}
134+
return { static_cast<kal_uintptr>(r), kal_ok };
135+
}
136+
}
137+
138+
void kal_datagram_close(kal_datagram d) {
139+
const int fd = fd_of(d);
140+
if (fd < 0) return;
141+
okl::sys(okl::nr_close, fd);
142+
okl::retire(d.h);
143+
}
144+
145+
// Broadcast is not claimed. The kernel provides it only after SO_BROADCAST has
146+
// been set, and this interface has no operation that would set it; a word
147+
// claiming a facility no operation reaches is the disagreement clause 6.2 exists
148+
// to prevent.
149+
const kal_uintptr kal_datagram_props = KAL_DGRAM_PROP_IPV6;
150+
151+
} // extern "C"

src/endpoint.h

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Conversion between kal_endpoint and the kernel's socket address structures.
2+
//
3+
// SHARED BY openkal.net AND openkal.datagram BECAUSE THE TYPE IS. Either
4+
// interface may be provided without the other, so the conversion belongs to
5+
// neither; writing it twice would be one decision stated in two places, and the
6+
// two would eventually disagree about which lengths are accepted.
7+
#pragma once
8+
#include "sys.h"
9+
#include <openkal/types.h>
10+
11+
namespace okl {
12+
13+
// The port is carried in host order by kal_endpoint and in network order by the
14+
// kernel. The conversion is written out rather than taken from a C library's
15+
// htons, for the reason the head of sys.h gives.
16+
inline unsigned short to_net_port(kal_u32 port) {
17+
const unsigned short p = static_cast<unsigned short>(port & 0xffffu);
18+
return static_cast<unsigned short>((p << 8) | (p >> 8));
19+
}
20+
inline kal_u32 from_net_port(unsigned short net) {
21+
return static_cast<kal_u32>((net << 8) | (net >> 8)) & 0xffffu;
22+
}
23+
24+
// Fills a kernel address from an endpoint, and reports its length.
25+
//
26+
// A LENGTH THIS IMPLEMENTATION DOES NOT KNOW IS REFUSED RATHER THAN READ AS ONE
27+
// IT DOES. The specification defines the set of lengths and allows it to grow;
28+
// an implementation that ignored the field would misread every address a later
29+
// revision defines, and would do so silently.
30+
inline int to_kernel(const kal_endpoint& ep, ksockaddr_storage& out, okl_long& len) {
31+
for (auto& b : out.pad) b = 0;
32+
33+
if (ep.addr_len == 4) {
34+
auto* v4 = reinterpret_cast<ksockaddr_in*>(&out);
35+
v4->family = af_inet;
36+
v4->port = to_net_port(ep.port);
37+
okl_u32 a = 0;
38+
for (int i = 0; i < 4; ++i)
39+
a |= static_cast<okl_u32>(ep.addr[i]) << (i * 8); // already network order
40+
v4->addr = a;
41+
for (auto& z : v4->zero) z = 0;
42+
len = static_cast<okl_long>(sizeof(ksockaddr_in));
43+
return kal_ok;
44+
}
45+
46+
// Sixteen bytes is an address; twenty is an address followed by a scope
47+
// identifier, which is carried in the four bytes after it.
48+
if (ep.addr_len == 16 || ep.addr_len == 20) {
49+
auto* v6 = reinterpret_cast<ksockaddr_in6*>(&out);
50+
v6->family = af_inet6;
51+
v6->port = to_net_port(ep.port);
52+
v6->flowinfo = 0;
53+
for (int i = 0; i < 16; ++i) v6->addr[i] = ep.addr[i];
54+
okl_u32 scope = 0;
55+
if (ep.addr_len == 20)
56+
for (int i = 0; i < 4; ++i)
57+
scope |= static_cast<okl_u32>(ep.addr[16 + i]) << (i * 8);
58+
v6->scope_id = scope;
59+
len = static_cast<okl_long>(sizeof(ksockaddr_in6));
60+
return kal_ok;
61+
}
62+
63+
return kal_err_invalid;
64+
}
65+
66+
// Fills an endpoint from a kernel address. A family this implementation does
67+
// not know leaves the endpoint zeroed and reports it, for the same reason.
68+
inline int from_kernel(const ksockaddr_storage& in, kal_endpoint& out) {
69+
for (auto& b : out.addr) b = 0;
70+
out.addr_len = 0;
71+
out.port = 0;
72+
73+
if (in.family == af_inet) {
74+
const auto* v4 = reinterpret_cast<const ksockaddr_in*>(&in);
75+
const okl_u32 a = v4->addr;
76+
for (int i = 0; i < 4; ++i)
77+
out.addr[i] = static_cast<kal_u8>((a >> (i * 8)) & 0xffu);
78+
out.addr_len = 4;
79+
out.port = from_net_port(v4->port);
80+
return kal_ok;
81+
}
82+
83+
if (in.family == af_inet6) {
84+
const auto* v6 = reinterpret_cast<const ksockaddr_in6*>(&in);
85+
for (int i = 0; i < 16; ++i) out.addr[i] = v6->addr[i];
86+
// A zero scope identifier is reported as the shorter form. The two
87+
// lengths denote the same address when the scope is zero, and reporting
88+
// the shorter one keeps an address that came in as sixteen bytes going
89+
// back out as sixteen.
90+
if (v6->scope_id == 0) {
91+
out.addr_len = 16;
92+
} else {
93+
for (int i = 0; i < 4; ++i)
94+
out.addr[16 + i] = static_cast<kal_u8>((v6->scope_id >> (i * 8)) & 0xffu);
95+
out.addr_len = 20;
96+
}
97+
out.port = from_net_port(v6->port);
98+
return kal_ok;
99+
}
100+
101+
return kal_err_invalid;
102+
}
103+
104+
// Which socket family an endpoint asks for, or -1 for a length that is not one
105+
// of the defined ones.
106+
inline okl_long family_of(const kal_endpoint& ep) {
107+
if (ep.addr_len == 4) return af_inet;
108+
if (ep.addr_len == 16 || ep.addr_len == 20) return af_inet6;
109+
return -1;
110+
}
111+
112+
} // namespace okl

0 commit comments

Comments
 (0)