Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -717,6 +717,88 @@ jobs:
# unconditionally would fail to link against a conforming implementation
# of three. Selecting three is therefore asserted to produce a report
# rather than a link failure.
# openkal-kit, which is in this repository and is not this specification.
#
# The kit composes facilities from openkal's interfaces --- awaiting a
# channel, writing an endpoint down and reading one back --- that the
# specification declines to admit because they can be composed. Running its
# tests here is what keeps the composition working as the interfaces it
# rests upon change.
#
# THE SURFACE CHECK ABOVE IS WHAT KEEPS THE TWO APART, and it does so by
# the shape of what is exported rather than by a declaration. The kit is
# C++ modules in `namespace kal::kit` and exports no name beginning with
# `kal_`; a C++ name is mangled to `_ZN3kal3kit…`, so an implementation
# that linked the kit still passes `check-surface.sh --complete`.
- name: The kit, composed from the interfaces above
if: matrix.implementation == 'openkal-linux'
run: |
bash tools/run-kit-tests.sh '${{ matrix.implementation }}' .impl

# ⭐⭐ THE EXAMPLE A CONSUMER READS, BUILT AND RUN.
#
# examples/portable is the program the specification points at as the one
# every implementation must satisfy, and NOTHING BUILT IT. Its README
# stated that "an implementation's continuous integration fetches this
# source ... builds it against itself, and asserts the lines below";
# measured across all six implementation repositories, the word `portable`
# appeared in none of their workflows. A promise with nothing executing it.
#
# ⚠️ AND THE DRIFT THAT FOLLOWS FROM THAT IS VISIBLE IN ITS OWN MANIFEST:
# it named `openkal = "0.5.0"` --- three specification versions behind ---
# while claiming to demonstrate the current contract to a reader who would
# copy it. Nothing could have noticed.
#
# The manifest names published versions, because that is what a consumer
# writes. Those do not exist while a change spanning these repositories is
# under review, so the working trees stand in, exactly as
# tools/run-conformance.sh does for the suite.
#
# ⚠️ RELATIVE PATHS, NOT `pwd`. This job runs on Windows too, where MSYS
# reports `/d/a/...`, which the build tool does not resolve. Relative
# paths also cannot put a local directory into a file that might be
# committed.
#
# ⚠️ NO `standalone` FEATURE HERE. That feature makes the implementation
# supply `_start`, which collides with the C library's crt1.o on a hosted
# toolchain --- measured: "multiple definition of `_start`". The
# conformance runner defaults to no features for the same reason.
- name: The portable program builds and runs over this implementation
working-directory: examples/portable
run: |
set -euo pipefail
subst() { sed -E "$2" "$1" > "$1.next" && mv "$1.next" "$1"; }
subst mcpp.toml 's|^openkal = .*$|openkal = { path = "../.." }|'
subst mcpp.toml 's|^${{ matrix.implementation }} = .*$|${{ matrix.implementation }} = { path = "../../.impl" }|'
grep -q 'path = "../../.impl"' mcpp.toml \
|| { echo "::error::the implementation was not substituted"; exit 1; }

# ⚠️ THE IMPLEMENTATION REACHES THE SPECIFICATION TOO, AND BY ITS OWN
# FORM. Substituting only this manifest leaves two routes to one
# package and the build refuses, correctly:
#
# error: dependency 'mcpplibs.openkal' is requested as both a path
# dep (by 'portable') and a git dep (by 'openkal-linux@path')
#
# Both must name the same directory, which is what run-conformance.sh
# does for the suite. From `.impl/` the specification is `..`; the two
# spellings resolve to one canonical path and therefore to one node.
subst ../../.impl/mcpp.toml 's|^openkal = .*$|openkal = { path = ".." }|'
grep -q 'openkal = { path = ".." }' ../../.impl/mcpp.toml \
|| { echo "::error::the implementation still reaches openkal its own way"; exit 1; }

mcpp run 2>&1 | tee portable.log

# ⭐ THE COUNT IS THE CRITERION, AND SO IS THE DENOMINATOR. A program
# that failed to start would print none of these lines, and a check
# for the absence of failures would hold over an empty log.
lines=$(grep -c '^openkal: ' portable.log)
[ "$lines" -ge 10 ] \
|| { echo "::error::the program printed $lines lines; it reports one per interface"; exit 1; }
grep -q 'openkal: observations that did not hold: 0' portable.log \
|| { echo "::error::the portable program reported observations that did not hold"; exit 1; }
echo " the portable program held every observation over ${{ matrix.implementation }}"

- name: A selection of three interfaces is examined, not refused
run: |
rm -rf conformance/target
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,40 @@ under the compiler its author used is a description of that compiler.
## License

Apache-2.0.

## openkal-kit

`kit/` holds facilities composed from the interfaces this specification defines.
It is a separate package, `mcpplibs/openkal-kit`, and it is **not** part of the
specification.

The specification admits an interface only when it is a minimal capability every
kernel has and cannot be composed from the interfaces already present. That rule
is what keeps openkal implementable on a machine with firmware and nothing else,
and it leaves a gap: a program that wants to carry bytes between two of its own
contexts, or to turn `"127.0.0.1:8080"` into an endpoint, has an answer in POSIX
and no answer here — because both are composed rather than primitive.

That gap was being filled by the port layer. There is one port layer today and
what it composes is POSIX, so a native openkal program either wrote the
composition again or took a whole C library. The kit is where the composition is
written once.

**The contract form is what makes the two unmistakable.** Clause 10 states that
openkal's contract is a C application binary interface. The kit deliberately is
not one: it is C++ modules in `namespace kal::kit`, and it exports no name
beginning with `kal_`. Measured on its objects: the defined names are C++ mangled
module initialisers such as `_ZGIW7openkalW3kitW7channel`, and the operations are
inline and emitted into consumers rather than exported at all. So
`tools/check-surface.sh --complete` does not read a program that links the kit as
an implementation which has added names — the rule that checker enforces is about
the C surface, and the kit has none.

So "is this normative" is answered by the shape of what is exported rather than
by a sentence saying it is not. A sentence can be overlooked; a mangled name
cannot become a C symbol.

The consequence is the one that matters. Clause 8 forbids the specification from
altering a declaration it has published, which is what makes openkal safe to
depend upon and what makes it the wrong place for a facility still finding its
shape. **The kit may evolve.**
89 changes: 86 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# openkal Specification, version 0.7
# openkal Specification, version 0.8

## 1. Scope

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -339,7 +352,9 @@ This clause adds no declaration and alters none, so clause 8's rule is not
engaged and the version does not advance on its account. Every declaration of
version 0.6 already satisfies it — one hundred and one of them, examined by the
procedure below — and what is new is that the property is now stated and
checked rather than held by the care of whoever wrote each header.
checked rather than held by the care of whoever wrote each header. The five
interfaces added in version 0.8 were examined by the same procedure and satisfy
it also; the count is now one hundred and forty-six.

Clause 9's procedure examines this.

Expand Down Expand Up @@ -413,6 +428,44 @@ consumer imports, which contradicts clause 4.
implementation declares them. Not adopted: a record can disagree with the code
it describes, and the file is a second place in which a package is configured.

Two further arrangements were weighed while specifying version 0.8 and are
recorded on the same basis.

**Readiness notification.** An interface reporting that a stream may be read, by
waking a word as `kal_task_wake` does, was considered as the remedy for a context
that would otherwise wait without end. It composes better than the bound this
specification adopted: one operation covers every waitable resource, a single
context may await many sources, and a library above it needs no read-ahead buffer
because a notification consumes nothing.

It was not adopted because of what it asks of an implementation. On an
environment whose readiness is discovered by polling a set of descriptors, an
implementation would have to maintain that set and a context of its own to watch
it. That is a mechanism reconstructed rather than a facility conveyed, which
clause 7.1 excludes. `openkal.timeout` asks the same environment only for what it
already does at the point of the call.

**A space as a handle.** An earlier form of `openkal.space` separated the copying
of an address space from the starting of a context in it, so that a caller held a
space and could start a context in it afterwards. It was withdrawn while the
first implementation was being written.

No environment this specification targets has that pair as a primitive. The copy
and the start are one act, and an implementation asked to separate them would
have to start a context anyway, park it upon a waiting primitive, and build a
channel by which to tell it what to run. Clause 7.1 identifies that as a fault in
the shape of the specification rather than in the implementation, and the
separated form was the shape at fault. The single operation that replaced it is
what every such environment already performs.

**An instant rather than a duration.** `openkal.timeout` states a duration
because `kal_task_wait` does. An instant would not accumulate drift when a caller
retries in a loop, and was considered for that reason. It was not adopted because
it would give one specification two spellings of one idea. A caller that requires
an instant computes the remaining duration from `kal_time_monotonic`, so the cost
falls upon the caller that has the requirement rather than upon every
implementation.

The measurements that constrain any future proposal:

1. A requires-expression naming a qualified entity that does not exist is
Expand Down Expand Up @@ -787,3 +840,33 @@ The following are recorded so that they are not mistaken for oversights.
in its symbol names. Clause 8 protects the interface by prohibiting change
rather than by permitting coexistence, and an ecosystem that outgrows that
prohibition will require a mechanism this version does not define.
4. **Readiness.** Awaiting one of several sources is not an operation of this
specification. It is reached above the interface, from `openkal.task` and a
bound upon each wait; clause 6.3 records the alternative that was weighed and
the property of implementations that excluded it.
5. **Name resolution.** `openkal.net` and `openkal.datagram` carry an address and
a port. Turning a name into one is excluded by clause 3.4 and remains so: an
implementation shall not be required to parse an unbounded set of name
schemes.
6. **Permission and ownership of files.** Not defined, and not a deferral. A
permission presupposes an identity, and the environments this specification
targets do not agree that one exists. A C library above openkal reports the
absence as the error its own surface defines.
7. **Creation and reading of links.** Not defined, and not a deferral, for the
reason clause 6.4 gives: whether a filesystem has links is a property of the
format rather than of the environment. `KAL_FS_PROP_LINKS` reports it, and
resolution follows one where the property is claimed.
8. **Duplication of the calling image.** `fork` is refused by clause 7.1 and that
refusal stands. It is refused as an OPERATION. The atomic capabilities from
which a library may compose it are specified: `openkal.space` clones an
address space and starts a context in one, and `KAL_SPACE_PROP_CLONE_HANDLES`
states whether the handles accompany the memory. What this specification
declines to do is duplicate execution state, which a library above the
interface performs with the compiler's own facilities. A sentence reading
"openkal will not have fork" would have buried that distinction, and this
entry exists so that it is not written.
9. **Transfer of a handle between address spaces.** `kal_space_start` conveys no
handle, and `kal_process_channel` conveys a stream only across a spawn. A
general mechanism for passing a handle to a context in another space is not
defined by this version. It is the question `openkal.space` reaches first and
is not peculiar to it.
39 changes: 38 additions & 1 deletion SURFACE.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -75,3 +78,37 @@ kal_exec_alloc
kal_exec_free
kal_exec_props
kal_exec_publish
# openkal.terminal
kal_terminal_get_mode
kal_terminal_props
kal_terminal_set_mode
kal_terminal_size
# openkal.net
kal_net_accept
kal_net_close
kal_net_close_listener
kal_net_connect
kal_net_listen
kal_net_listener_local
kal_net_local
kal_net_peer
kal_net_props
kal_net_shutdown
kal_net_stream
# openkal.datagram
kal_datagram_close
kal_datagram_local
kal_datagram_open
kal_datagram_props
kal_datagram_recv_from
kal_datagram_send_to
# openkal.space
kal_space_props
kal_space_start
# openkal.timeout
kal_timeout_accept
kal_timeout_granularity_ns
kal_timeout_read
kal_timeout_recv_from
kal_timeout_wait_process
kal_timeout_write
16 changes: 13 additions & 3 deletions conformance/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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.
#
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading