Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3272721
openkal-musl 0.1.0: musl 1.2.5 above openkal, and the probe it replaces
Aug 19, 2026
edfbc60
openkal-musl: state the arrangement in openkal's terms, and link unde…
Aug 20, 2026
d1bf9c8
openkal-musl: musl above openkal on Windows, and what that cost
Aug 20, 2026
2922273
openkal-musl: name the published versions again
Aug 20, 2026
7fd2be4
openkal-musl: macOS, three operations expressed differently, and the …
Sunrisepeak Aug 20, 2026
fc84e84
openkal-musl: name this system's own linker, for the fifteen indirect…
Sunrisepeak Aug 20, 2026
b54682f
openkal-musl: keep the definition a second name is made for
Sunrisepeak Aug 20, 2026
541c153
openkal-musl: give the four names something to name on the third obje…
Sunrisepeak Aug 20, 2026
4042ba1
ci: ask the system where a program that stopped without printing was
Sunrisepeak Aug 20, 2026
d906bb1
docs: three object formats, three operations, and the measurements as…
Sunrisepeak Aug 20, 2026
301a856
openkal-musl: move a context identity of zero out of the table's way
Sunrisepeak Aug 20, 2026
bfff2ab
docs: 记录实施结果与方案的偏离
Sunrisepeak Aug 20, 2026
ef75168
ci: keep the whole of what the debugger said, and name the commit eac…
Sunrisepeak Aug 20, 2026
1b58b98
probe: report each observation as it is made, so that a run that stop…
Sunrisepeak Aug 20, 2026
c9c3d72
openkal-musl: say that a context has no state, rather than answering …
Sunrisepeak Aug 20, 2026
a3340b3
openkal-musl: a program above this library must not have a C++ runtim…
Sunrisepeak Aug 20, 2026
361c040
ci: a program that does not return is a failure, and its partial outp…
Sunrisepeak Aug 20, 2026
371bbeb
chore: do not carry what is not the package
Sunrisepeak Aug 20, 2026
a09039a
ci: a program that hangs and a program that stops need different ques…
Sunrisepeak Aug 20, 2026
c026f95
openkal-musl: long double is not a property of the architecture alone
Sunrisepeak Aug 20, 2026
b9844f3
openkal-musl: the specification at the version that is published
Sunrisepeak Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
60 changes: 59 additions & 1 deletion .agents/docs/2026-08-20-musl-on-openkal-plan.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,64 @@
# 在 openkal 之上重定向 musl:实施方案与验证判据

**状态**:方案,待 review。尚未实施。
**状态**:已实施。本节记录结果与方案的偏离;其余各节保留为当初的判断记录,不作追改,
因为一份被结果修改过的方案不再能说明结果是否被预见。

## 0. 结果

| 方案 | 结果 |
| --- | --- |
| 目标系统 Linux 与 macOS | Linux、macOS、Windows 三个系统 |
| 「musl 目前只支持 Linux」 | 成立,且三个系统上的 CI 各自运行同一份源码 |
| 接缝为 `syscall_arch.h` 七个内联函数 | 成立,未增减 |
| musl 自身源码不改 | 四行改动,九个源文件被替换,逐条记于 `musl/PATCHES.md` |
| port 层规模约 2000 行 | 2793 行,增量全部关于目标文件格式与命名约定,不关于内核 |
| openkal 不因 musl 而改变形状 | 不成立:openkal 增加了五个错误值、五个操作与两条命名规则 |

最后一行是本方案唯一的实质性误判,值得单独说明。方案主张 POSIX 在 port 层被重建、
openkal 保持不变。前半句成立;后半句不成立,并且不成立的方式与「为 musl 让步」无关 ——
增加的每一项都是**任何**位于 openkal 之上的 C 库都需要而当时的接口无法表达的:

- `kal_err_not_found` 等五个错误值:一个 C 库必须把 `ENOENT` 与 `EEXIST` 区分开,
0.4 把两者映射到同一个值;
- `kal_fs_open`:创建、排他、截断、追加是**打开这件事**的条件而非其后的操作,
分两步做的程序可以在两步之间被停止;
- `kal_fs_truncate`、`kal_fs_file_info`:对已打开文件的操作无法用名字表达,
因为名字此刻可能已指向别的东西;
- `kal_fs_set_modified`:接口报告一个时间而不能设定它,则复制文件并保留日期、
展开归档、把文件标记为当前这三类程序都写不出来;
- `KAL_STREAM_PROP_INTERACTIVE`:C 库必须在传输任何字节之前选定缓冲策略;
- `KAL_TASK_PROP_THREAD_LOCAL`:C 库把每上下文状态放在何处,取决于启动的上下文
是否观察到工具链的线程局部存储,而这一点无法由操作探知;
- `"."` 为保留名:程序持有一个目录却无法就该目录本身发问,因为所有发问的操作都取名字。

也就是说,把一份真实的 C 库放到 openkal 之上,所暴露的是**接口的不完备**而不是
musl 的特殊性。这正是 §7 所说的完备性判据在实践中的形态:判据不是「移植成功」,
而是「移植过程中接口被迫增加了什么,以及每一项是否对所有消费者都成立」。

三条在方案中未被预见、且只能由运行发现的结论:

1. 位于 C 库之下的 openkal 实现**不得借用任何 C 库**。程序自身定义 `write`、`malloc`、
`open`,实现若调用这些名字,调用解析到程序的定义,而程序的定义又调用实现 ——
递归无界,且在任何一侧的源码中都不可见。openkal-linux 与 openkal-macos 因此
改写为直接发出内核调用。
2. 一个 C 库**不能**把每上下文状态放在 `thread_local` 变量中。在三种工具链之一上,
这样的变量经由一个会分配内存的辅助函数到达,而分配器正是该 C 库提供的。
3. 一个上下文标识为零的实现,对键控于该标识的消费者而言等同于「没有条目」。
这一条由 macOS 上的一次崩溃发现,并已成为一致性套件的两条观察。

## 0.1 验证

| | |
| --- | --- |
| openkal 一致性套件 | 97 项观察,三个实现各自 97/97 |
| `examples/posix` | 32 项断言,三个系统 |
| `examples/wordcount` | 与系统自带 `wc` 的三个计数逐一相同 |
| `mcpplibs/sbase` | 97 个 suckless 工具,源码未改,50 项与系统自带工具的比对 |

最后一项是方案 §6.2 所要求的对照实验的实际形态。sbase 在 Windows 上没有任何移植,
在 macOS 上也不能按现状构建 —— 三个工具包含 `<sys/sysmacros.h>`,四个使用 `st_mtim`,
两者都是 Linux C 库的东西。这两处障碍都不在内核里,而在 C 库里,因而是一个 C 库能够
消除的那一类;它消除的方式是**存在**,而不是被适配。

**目标**:把 musl 1.2.5 重定向到 openkal 接口之上,使同一份 musl 源码在 Linux 与
macOS 两个操作系统上构建并通过其自身的测试套件。musl 目前只支持 Linux;若它经 openkal
Expand Down
183 changes: 137 additions & 46 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,81 +1,172 @@
name: CI

# What this workflow asserts.
#
# The claim this package makes is that a C library ported once onto openkal runs
# above every implementation of the specification, so the claim is tested by
# running the same two programs on three systems and comparing what they produce
# against what the system's own tools produce.
#
# posix a program that opens a file by an absolute name, reads a
# variable, measures an interval, starts another program and waits
# for it --- thirty-two observations, none of which the program
# contains an implementation of
# wordcount the same counts as the system's own `wc'
#
# Two compiler families, gcc and llvm. The third of the three mcpp offers is
# absent from this package and the reason is recorded in the manifest: musl's
# four remaining assembly definitions are in an object format that toolchain
# does not assemble, so the question that toolchain would answer is not one this
# package can ask.

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

env:
MCPP_VERSION: 2026.8.19.4
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'

jobs:
build:
name: conformance
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
# A version verified to build this package, not a measured minimum. The
# package uses modules, exported extern "C" declarations and ordinary
# dependencies, none of which is recent; the pin exists for reproducibility
# rather than because an older mcpp is known to fail.
MCPP_VERSION: 2026.8.19.4
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
programs:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- { name: 'linux, gcc', os: ubuntu-24.04, toolchain: 'gcc@16.1.0', target: '' }
- { name: 'linux, llvm', os: ubuntu-24.04, toolchain: 'llvm@22.1.8', target: '' }
- { name: 'macos, llvm', os: macos-14, toolchain: 'llvm@20.1.7', target: '' }
- { name: 'windows, gcc', os: windows-2022, toolchain: 'gcc@16.1.0', target: 'x86_64-windows-gnu' }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4

- name: Install xlings
- name: The specification and the implementation for this system
run: |
bash tools/working-trees.sh '${{ github.head_ref || github.ref_name }}'

- name: Install xlings (Unix)
if: runner.os != 'Windows'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install xlings (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
irm https://d2learn.org/xlings-install.ps1.txt | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Install mcpp
run: |
xlings update
xlings install "mcpp@$MCPP_VERSION" -y -g
mcpp --version
mcpp self config --mirror GLOBAL

- name: Tests
- name: Select the toolchain
run: |
spec='${{ matrix.toolchain }}'
mcpp toolchain install "${spec%@*}" "${spec#*@}"
mcpp toolchain default "$spec"
mcpp toolchain list

- name: The library builds
run: |
extra=''
[ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}'
mcpp build $extra

# A program above this package names one package. It does not name
# openkal, it does not name an implementation, and it says nothing about
# the platform.
- name: The posix probe
working-directory: examples/posix
run: |
mcpp test 2>&1 | tee tests.log

# A suite that discovered nothing reports success, so every suite is
# asserted to have run. The list is derived from the files present
# rather than written out here: a hand-written list names the suites
# that existed when it was written, and one added afterwards escapes
# the assertion silently.
missing=0
for f in tests/*.cpp; do
name="$(basename "$f" .cpp)"
if ! grep -q "^$name \.\.\. ok" tests.log; then
echo "suite did not run, or did not pass: $name" >&2
missing=1
extra=''
[ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}'
mcpp build $extra
binary="$(find target -type f \( -name 'posix' -o -name 'posix.exe' \) | head -1)"

# A watchdog, because a program that does not return is as much a
# failure as one that returns wrongly, and the job would otherwise
# spend its whole timeout finding that out. Written out rather than
# taken from `timeout', which two of the three systems have and one
# does not.
watch() { # watch <seconds> <command>...
local seconds="$1"; shift
"$@" & local pid=$!
( sleep "$seconds"; kill -9 "$pid" 2> /dev/null ) & local guard=$!
wait "$pid"; local status=$?
kill "$guard" 2> /dev/null || true
return $status
}

if watch 120 sh -c "\"$binary\" > run.log 2>&1"; then
cat run.log
else
status=$?
echo "--- what the program printed before it stopped (status $status) ---"
cat run.log

# A program that stopped and a program that did not return need
# different questions asked of them, and "exit code 139" and "the
# job timed out" answer neither. The debugger is for the first; a
# stack sample of a program that is still running is for the second,
# and a debugger asked to run a program that hangs hangs with it.
if [ "$status" -eq 137 ]; then
echo "--- it did not return; where it was ---"
"$binary" > /dev/null 2>&1 & hung=$!
sleep 5
if command -v sample > /dev/null 2>&1; then
sample "$hung" 3 -mayDie 2>&1 | head -80 || true
elif command -v eu-stack > /dev/null 2>&1; then
eu-stack -p "$hung" 2>&1 | head -60 || true
fi
kill -9 "$hung" 2> /dev/null || true
elif command -v lldb > /dev/null 2>&1; then
watch 90 lldb --batch -o run \
-k 'thread backtrace all' -k 'register read' -k quit \
-- "$binary" > crash.log 2>&1 || true
cat crash.log
elif command -v gdb > /dev/null 2>&1; then
watch 90 gdb -batch -ex run -ex 'bt' --args "$binary" > crash.log 2>&1 || true
cat crash.log
fi
done
test "$missing" -eq 0

# The claim this package makes is that a program above it reads a file by
# global path, consults a variable, measures an interval and starts
# another program without containing any of that. The program is compared
# against the system's own counter, so the assertion is that it produced
# the right answer rather than that it produced an answer.
#
# The previous form of this step asserted three lines from a different
# example, in a directory this repository does not contain. It had never
# run: the step before it failed first, and a step that never runs asserts
# nothing while appearing to.
- name: An ordinary program runs above the library
exit 1
fi
# Both directions: that the program reported, and that nothing it
# observed failed to hold. Asserting only the first would pass for a
# program that printed its failures.
grep -qE '^-- failures: 0 --$' run.log
! grep -q '^FAIL:' run.log

- name: The counts agree with the system's own
working-directory: examples/wordcount
run: |
printf 'alpha beta\ngamma\n' > sample.txt
expected="$(wc < sample.txt | tr -s ' ' | sed 's/^ //')"
WORDCOUNT_VERBOSE=1 mcpp run -- sample.txt 2>&1 | tee run.log
extra=''
[ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}'
mcpp build $extra
binary="$(find target -type f \( -name 'wordcount' -o -name 'wordcount.exe' \) | head -1)"
WORDCOUNT_VERBOSE=1 "$binary" sample.txt 2>&1 | tee run.log

lines="$(sed -n 's/^lines \([0-9]*\) words \([0-9]*\) bytes \([0-9]*\)$/\1 \2 \3/p' run.log)"
test -n "$lines" || { echo "the program printed no count" >&2; exit 1; }
echo "wordcount: $lines"
counts="$(sed -n 's/^lines \([0-9]*\) words \([0-9]*\) bytes \([0-9]*\)$/\1 \2 \3/p' run.log)"
test -n "$counts" || { echo "the program printed no count" >&2; exit 1; }
echo "wordcount: $counts"
echo "wc: $expected"
test "$lines" = "$expected"
test "$counts" = "$expected"

# The variable was consulted, and the interval was measured.
grep -q 'elapsed .* nanoseconds' run.log
21 changes: 20 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
# What a build produces.
target/
.mcpp/
compile_commands.json
mcpp.lock
compile_commands.json

# What an editor or a tool leaves beside the sources. None of it describes the
# package: a repository that carries one editor's settings has an opinion about
# editors, and a repository that carries a compilation database has an opinion
# about one machine's paths.
.vscode/
.idea/
.cache/
*.swp
*~

# What a system leaves behind.
.DS_Store
Thumbs.db

# What the examples write while they run.
sample.txt
*.tmp
Loading
Loading