Skip to content

Commit 71afa0d

Browse files
committed
openkal-emscripten 0.1.0: an implementation written above a C library
Every other implementation in this ecosystem is written on a kernel's own interface. Emscripten has no kernel to issue a call to: it has a C library over a JavaScript host, and clause 2 permits exactly this arrangement in as many words -- "an implementation may be built upon a C library, beneath one, or without one". This is the first here to take the first of the three. That direction makes the code thin and not easy. What an above-libc implementation has to get right is the places where the C library's vocabulary and openkal's do NOT correspond, and each is commented where it occurs: fs openkal has no ambient working directory, which `openat` matches exactly; one preopen at `/`, because a platform with no command line has no one to say which; `remove` is one operation over `unlink` and `rmdir`, retried only on the condition that names the other kind; `.` and `..` are not entries, because openkal has no parent relation memory the granularity is the alignment honoured and not wasm's 64 KiB page, which is the unit linear memory GROWS in; a recorded defect in this ecosystem is a C library reading a truthful granularity of 1 as a page size time the monotonic granularity is MEASURED rather than asked for: `clock_getres` answers a nanosecond and that is about the struct, while a browser deliberately coarsens `performance.now()` random `getentropy` is the host's cryptographic generator; MEMFS's `/dev/urandom` is a different source reached through the file system terminal asked of the machine per call, so one module answers correctly under node and in a browser without a build-time switch net the calls are real and the transport is a WebSocket proxy, so the interface is provided and `kal_net_props` claims nothing -- HALFCLOSE in particular, because `shutdown` succeeds and no peer is told timeout `poll` then the operation, and the granularity is `poll`'s millisecond rather than the clock's THREE INTERFACES ARE NOT PROVIDED AND THE ABSENCE IS THE REPORT. `openkal.process`, `openkal.exec` and `openkal.space`: there is no fork, no exec, no writable-then-executable memory and no second address space. Measured: wasm-ld: error: obj/main.o: undefined symbol: kal_process_spawn which is clause 6.2's second time. Providing them so that they returned an error would be the shape the specification forbids -- present and always failing, which the caller cannot tell from a condition -- and would move a fact known at link time to run time. THE ARGUMENTS TOOK THREE ATTEMPTS AND THE PLATFORM REFUSED TWO. `.init_array` with a `(argc, argv, envp)` signature does not link -- "constructor functions cannot take arguments", because wasm's start section takes none. A weak interposer on the entry point has no seam: the name belongs to the program. The host has them, and `EM_ASM` reaches it; index zero also has a JS-free source in musl's `__progname_full`, so a browser still reports the name it was started by. The first version of that copy used `stringToUTF8` into a `_malloc` scratch buffer and aborted with "malloc() called but not included in the build" -- a library cannot require an export list from every program that uses it -- so it uses `TextEncoder` and `HEAPU8.set`, which need nothing exported. `openkal.task` IS CARRIED BY A FEATURE, AND THE MEASUREMENT DECIDED THAT. Threads need `-pthread`, which selects a different C library build, memory model and loader contract. With the interface compiled and `kal_task_start` reporting kal_err_not_supported, the suite said: DID NOT HOLD an execution context starts DID NOT HOLD four execution contexts start DID NOT HOLD contexts that ran at the same time have identities distinct and the suite is right: a capability word is for how an interface behaves, not for whether its central operation works at all. So without the feature the translation unit is empty, the eight symbols do not exist, and `kal_interfaces()` -- keyed on the compiler's own `__EMSCRIPTEN_PTHREADS__` rather than on a name this package invented -- does not claim it. The feature is not yet usable end to end because `-pthread` must reach every package in the graph including the specification's, and mcpp has no whole-graph flag channel; that limitation is recorded in the README rather than worked around. `handle.h` IS COPIED FROM openkal-linux AND ITS CONSTANT WAS NOT. That file splits the handle word at bit 32, which is the whole width of a pointer here: handle.h:22: warning: shift count >= width of type The index width is now derived from `kMaxDescriptor` and the generation takes what is left -- 15 bits here, 47 on a 64-bit host, from one expression. The generation wraps sooner as a result, and that is stated rather than left to be inferred from the other implementation's numbers. CONFORMANCE, measured with emsdk 6.0.9 under node: 86 held, 0 did not hold, 13 not observed. The exported surface is 86 names, twelve complete groups, three absent groups, and nothing beginning with `kal_` the specification does not name.
0 parents  commit 71afa0d

348 files changed

Lines changed: 5632 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
name: CI
2+
3+
# What this workflow asserts.
4+
#
5+
# conformance the specification's suite runs against this implementation,
6+
# under node, for the twelve interfaces it provides, and every
7+
# observation holds
8+
# surface the exported names are exactly the eighty-six the
9+
# specification lists for those twelve groups, and nothing else
10+
# beginning with kal_
11+
# absence a program that uses one of the three interfaces this platform
12+
# does not have fails at LINK naming the symbol, which is
13+
# clause 6.2's second time and is the mechanism rather than a
14+
# defect
15+
# task gating the eight kal_task_* symbols exist in a `-pthread` link and
16+
# in no other, and the interfaces word follows the link
17+
#
18+
# ONE TOOLCHAIN, NOT TWO, AND THAT IS A PROPERTY OF THE TARGET RATHER THAN A
19+
# GAP. Every other implementation here is built by a matrix of compiler
20+
# families, because an implementation is a contract's evidence and a contract
21+
# that holds only under one compiler is a description of that compiler. This
22+
# target has one compiler: `em++` IS the toolchain, and there is no second
23+
# family that emits wasm for Emscripten's C library.
24+
25+
on:
26+
push:
27+
branches: [main]
28+
pull_request:
29+
workflow_dispatch:
30+
inputs:
31+
mcpp_ref:
32+
description: "Branch of mcpp-community/mcpp to build and test against (empty = the released pin)"
33+
required: false
34+
default: ""
35+
36+
env:
37+
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
38+
# THE CI PIN, NOT A FLOOR. Nothing here records a minimum engine for
39+
# consumers. This engine is required by two facts this repository depends on:
40+
# `em++`'s C compiler is `emcc` and not `em` (a C translation unit in the
41+
# conformance suite does not compile without that), and the wasm target rows
42+
# resolve the emsdk payload.
43+
MCPP_VERSION: 2026.9.11.4
44+
XLINGS_VERSION: v2026.8.17.2
45+
XLINGS_NON_INTERACTIVE: '1'
46+
47+
jobs:
48+
conformance:
49+
name: conformance, surface, absence, and the task gate
50+
runs-on: ubuntu-24.04
51+
timeout-minutes: 60
52+
steps:
53+
- uses: actions/checkout@v4
54+
55+
# THE COMMITTED MANIFEST NAMES NO DIRECTORY OF ANYBODY'S MACHINE.
56+
#
57+
# Two scripts in the specification's repository rewrite this manifest to
58+
# name a working tree, and both restore it through a trap. A trap does
59+
# not fire when the process is killed, and a run by hand followed by
60+
# `git add -A` then publishes a path that exists on one machine. That has
61+
# happened in this ecosystem, in openkal-musl. This step runs first, so
62+
# what it examines is what the commit contains.
63+
- name: The committed manifest names no local directory
64+
run: |
65+
set -euo pipefail
66+
bad=$(grep -nE '^[a-z-]+ = \{[^}]*path = "(/|[A-Za-z]:)' mcpp.toml || true)
67+
if [ -n "$bad" ]; then
68+
echo "::error::the committed manifest names an absolute path"
69+
printf '%s\n' "$bad" | sed 's/^/ /'
70+
exit 1
71+
fi
72+
echo " ok every dependency is named by version or a relative path"
73+
74+
- name: The specification
75+
run: |
76+
git clone --quiet https://github.com/mcpplibs/openkal.git .spec
77+
branch='${{ github.head_ref || github.ref_name }}'
78+
if git -C .spec rev-parse --verify --quiet "origin/$branch" > /dev/null; then
79+
git -C .spec checkout --quiet "origin/$branch"
80+
echo "the specification is at $branch"
81+
else
82+
echo "the specification has no $branch; its default branch is used"
83+
fi
84+
85+
- name: Install xlings
86+
run: |
87+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
88+
| bash -s "$XLINGS_VERSION"
89+
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
90+
91+
- name: Install mcpp
92+
run: |
93+
xlings update
94+
# THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does not
95+
# exist yet -- that is the whole point of MCPP_SOURCE_REF. Bootstrap
96+
# from whatever the index has; the block below replaces it.
97+
if [ -n "${MCPP_SOURCE_REF:-}" ]; then
98+
xlings install mcpp -y -g
99+
else
100+
xlings install "mcpp@$MCPP_VERSION" -y -g
101+
fi
102+
mcpp --version
103+
mcpp self config --mirror GLOBAL
104+
# CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE.
105+
#
106+
# Empty in the ordinary run. Set it and the same job runs against
107+
# that source, which is what lets an engine change be validated in a
108+
# real dependency graph before it is published rather than after.
109+
if [ -n "${MCPP_SOURCE_REF:-}" ]; then
110+
src="$RUNNER_TEMP/mcpp-src"
111+
[ -d "$src" ] || git clone --quiet --depth 1 \
112+
--branch "$MCPP_SOURCE_REF" \
113+
https://github.com/mcpp-community/mcpp.git "$src"
114+
# The clone's own workspace pin must not decide which mcpp builds
115+
# it: that pin does not move when mcpp is released, so a build
116+
# inside the checkout tries to install a version the index may no
117+
# longer carry.
118+
rm -f "$src/.xlings.json"
119+
( cd "$src" && mcpp build --release )
120+
built=$(find "$src/target" -type f \
121+
\( -name mcpp -o -name mcpp.exe \) | head -1)
122+
[ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; }
123+
echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH"
124+
echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)"
125+
fi
126+
127+
# THE TOOLCHAIN IS CHOSEN BY THE TARGET AND NOT BY THIS FILE. The
128+
# `wasm32-emscripten` row pins `emsdk@6.0.9`, so naming a toolchain here
129+
# would be naming the same thing twice -- and the row's pin is the thing
130+
# under test.
131+
- name: The emsdk payload and a runner for wasm
132+
run: |
133+
set -euo pipefail
134+
mcpp toolchain install emsdk 6.0.9
135+
xlings install node -y -g || true
136+
node_bin=$(find "$HOME/.xlings" -path '*/xim-x-node/*/bin/node' | head -1)
137+
[ -n "$node_bin" ] || { echo "::error::no node payload"; exit 1; }
138+
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
139+
"$node_bin" --version
140+
nm_bin=$(find "$HOME/.mcpp" "$HOME/.xlings" -name llvm-nm -path '*emsdk*' | head -1)
141+
[ -n "$nm_bin" ] || { echo "::error::no llvm-nm in the emsdk payload"; exit 1; }
142+
echo "NM=$nm_bin" >> "$GITHUB_ENV"
143+
144+
# CONFORMANCE, FOR THE GROUPS THIS IMPLEMENTATION PROVIDES.
145+
#
146+
# The set is named rather than `full`: clause 6.1 says an implementation
147+
# provides an interface in whole or not at all, and a group this platform
148+
# does not have is an absence rather than a deviation. `task` is not in
149+
# the set -- see the task-gate step below for why it needs a link this
150+
# suite cannot currently be given.
151+
- name: Every interface this platform has, examined
152+
run: |
153+
OPENKAL_CONFORMANCE_RUNNER="$NODE_BIN" \
154+
bash .spec/tools/run-conformance.sh openkal-emscripten . \
155+
core,env,time,random,fs,terminal,timeout \
156+
--target wasm32-emscripten
157+
158+
# ABSENCE IS THE REPORT, AND THIS IS THE OBSERVATION THAT MAKES IT ONE.
159+
#
160+
# A program using an interface this platform does not have must fail at
161+
# LINK naming the symbol. Asserted by BUILDING such a program and
162+
# requiring the failure to name it -- not by grepping the sources, which
163+
# would pass for an implementation that defined the symbol elsewhere.
164+
- name: A program that spawns a process fails at link, naming the symbol
165+
run: |
166+
set -uo pipefail
167+
work="$RUNNER_TEMP/absent"
168+
rm -rf "$work"; mkdir -p "$work/src"
169+
impl="$(pwd)"
170+
# BOTH DEPENDENCIES NAMED THE SAME WAY, and this step therefore runs
171+
# BEFORE the manifest is pointed at the specification's working tree.
172+
# mcpp refuses a graph in which one package reaches `openkal` by path
173+
# and another by version -- measured:
174+
#
175+
# error: dependency 'mcpplibs.openkal' is requested as both a path
176+
# dep (by 'absent') and a version dep (by
177+
# 'mcpplibs.openkal-emscripten@path'). Pick one.
178+
#
179+
# so the consumer uses the version this implementation's committed
180+
# manifest names, which is the arrangement an ordinary consumer has.
181+
cat > "$work/mcpp.toml" << TOML
182+
[package]
183+
name = "absent"
184+
version = "0.1.0"
185+
186+
[dependencies]
187+
openkal = "$(grep -oP '^openkal = "\K[0-9.]+' "$impl/mcpp.toml")"
188+
openkal-emscripten = { path = "$impl" }
189+
TOML
190+
cat > "$work/src/main.cpp" << 'CPP'
191+
import openkal.process;
192+
import openkal.fs;
193+
int main() {
194+
kal_dir root{};
195+
const kal_spawn how{ root, root, nullptr, nullptr, 0, 0 };
196+
kal_process p{};
197+
return kal_process_spawn(&how, "x", 1, nullptr, nullptr, 0,
198+
nullptr, nullptr, 0, nullptr, &p);
199+
}
200+
CPP
201+
out=$( cd "$work" && mcpp build --target wasm32-emscripten 2>&1 ) || true
202+
# The linker's line is long; only the diagnostic is wanted.
203+
printf '%s\n' "$out" | grep -E "^(error|wasm-ld)" | head -5
204+
if printf '%s\n' "$out" | grep -q "kal_process_spawn"; then
205+
echo " ok the link names the symbol this platform does not have"
206+
else
207+
echo "::error::the build did not fail naming kal_process_spawn"
208+
exit 1
209+
fi
210+
211+
- name: Point at the specification's working tree
212+
run: |
213+
# run-conformance.sh has already done this; it is repeated because a
214+
# reader of this file should not have to know that.
215+
sed 's|^openkal = .*$|openkal = { path = ".spec" }|' mcpp.toml > mcpp.toml.next
216+
mv mcpp.toml.next mcpp.toml
217+
218+
# THE SURFACE, WITHOUT `--complete`. An absent name denotes an interface
219+
# this implementation does not provide, which clause 3 permits; what the
220+
# check still refuses is a name the specification does not list, which is
221+
# the one freedom an implementation retains.
222+
- name: The exported surface contains nothing the specification does not name
223+
run: |
224+
set -euo pipefail
225+
rm -rf target && mcpp build --target wasm32-emscripten
226+
objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' | tr '\n' ' ')
227+
[ -n "$objs" ] || { echo "::error::no objects were built"; exit 1; }
228+
bash .spec/tools/check-surface.sh .spec/SURFACE.txt $objs
229+
230+
# AND THE TWELVE GROUPS ARE COMPLETE, which the check above does not
231+
# assert: without `--complete` it permits every absence, so a group
232+
# missing one member would pass. The denominator is SURFACE.txt's own
233+
# headings, so a name added to the specification is covered without
234+
# editing this step.
235+
- name: Every group this implementation claims is whole
236+
run: |
237+
set -euo pipefail
238+
objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' | tr '\n' ' ')
239+
"$NM" --defined-only $objs 2>/dev/null \
240+
| awk '$2=="T"||$2=="W"||$2=="R"||$2=="D"||$2=="B"||$2=="S"{print $3}' \
241+
| sed 's/^_//' | grep '^kal_' | sort -u > /tmp/exported.txt
242+
awk '/^# openkal\./{g=substr($2,9); next} /^kal_/{print g" "$0}' \
243+
.spec/SURFACE.txt > /tmp/grouped.txt
244+
fail=0
245+
for g in version abort stream memory env time random fs terminal net datagram timeout; do
246+
missing=""
247+
while read -r s; do
248+
grep -qx "$s" /tmp/exported.txt || missing="$missing $s"
249+
done < <(awk -v g="$g" '$1==g{print $2}' /tmp/grouped.txt)
250+
if [ -n "$missing" ]; then
251+
echo "::error::openkal.$g is claimed and incomplete:$missing"; fail=1
252+
else
253+
echo " ok openkal.$g is whole"
254+
fi
255+
done
256+
# And the three this platform does not have export nothing at all.
257+
for g in process exec space; do
258+
present=""
259+
while read -r s; do
260+
grep -qx "$s" /tmp/exported.txt && present="$present $s"
261+
done < <(awk -v g="$g" '$1==g{print $2}' /tmp/grouped.txt)
262+
if [ -n "$present" ]; then
263+
echo "::error::openkal.$g is not provided yet exports:$present"; fail=1
264+
else
265+
echo " ok openkal.$g exports nothing"
266+
fi
267+
done
268+
test "$fail" -eq 0
269+
270+
# THE TASK GATE, IN BOTH DIRECTIONS.
271+
#
272+
# `openkal.task` needs `-pthread`, which selects a different C library
273+
# build and a different memory model -- a property of the whole link. So
274+
# the interface is carried by a feature: without it the translation unit
275+
# is empty and the eight symbols do not exist, which is the same
276+
# treatment the three absent interfaces get and for the same reason.
277+
#
278+
# Both directions are asserted, because a gate is only a gate if the
279+
# other side of it differs. The conformance suite cannot yet be built
280+
# with this switch -- it would have to reach the specification package's
281+
# modules too, and mcpp has no whole-graph flag channel -- so what is
282+
# asserted here is the gating, and the README records the rest.
283+
- name: The task interface exists in a -pthread link and in no other
284+
run: |
285+
set -euo pipefail
286+
rm -rf target
287+
mcpp build --target wasm32-emscripten
288+
objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' | tr '\n' ' ')
289+
n=$("$NM" --defined-only $objs 2>/dev/null | grep -c 'kal_task' || true)
290+
test "$n" -eq 0 || { echo "::error::$n kal_task symbols without the feature"; exit 1; }
291+
echo " ok no task symbol without the feature"
292+
293+
rm -rf target
294+
mcpp build --target wasm32-emscripten --features threads
295+
objs=$(find target -path '*/obj/*' -name '*.o' ! -name '*.m.o' | tr '\n' ' ')
296+
n=$("$NM" --defined-only $objs 2>/dev/null \
297+
| awk '$2=="T"||$2=="W"{print $3}' | sed 's/^_//' \
298+
| grep -c '^kal_task' || true)
299+
test "$n" -eq 8 || { echo "::error::expected 8 kal_task symbols with the feature, found $n"; exit 1; }
300+
echo " ok eight task symbols with the feature"
301+
302+
- name: Restore the manifest
303+
if: always()
304+
run: git checkout -- mcpp.toml

0 commit comments

Comments
 (0)