Skip to content

Commit 6a34941

Browse files
authored
0.4.0 —— 一个包、两个门面、三个后端 (#1)
* 0.4.0 —— 一个包、两个门面、三个后端,而第三台机器把门槛变成了证据 ## 第三台机器 riscv64 与 aarch64 都是弱内存序、定长指令的 load/store RISC 机器。一个同时适配两者 的接口,可能是因为它对,也可能是因为它们像 —— 在这两台机器上再怎么测也分不开这两种 情况。 x86_64 两样都不是:变长指令;total store order,四条屏障里有三条根本不需要指令;中断 机制是 256 个门的表而不是一个基址寄存器;控制台由 `out` 到达而不是由一次存储到达, 没有任何指针能命名它。**三台都活下来的才是抽象。** 一份探针源码在三台机器上构建并运行,输出逐字节相同。 ### 第三台机器改了什么 - **`MAIR_EL1` 那个决定不再是 aarch64 的例外。** 两台机器时是一比一,"这一层拥有属性 寄存器"还可以被称作 aarch64 的权宜。x86_64 做同一件事:`PWT`/`PCD`/`PAT` 三个分散的 位构成 `IA32_PAT` 的索引。现在是二比一,而且方向反了过来。 ⚠️ 它的规则更严:未编程的 `MAIR_EL1` 字段读作最严格的类型,过早的 aarch64 映射只是 慢而正确;`IA32_PAT` 的复位值在索引 1 上是 **write-through**,过早的设备映射是被缓存 的 —— 写在程序没有选择的时刻到达设备,且不触发任何异常。 - **`pc` 在每台机器上并不指同一件事,接口吸收了它而不是复述它。** 两台 RISC 机器都报告 出错指令的地址;x86_64 把异常分为 *fault*(如此)与 *trap*(报告**下一条**的地址), 而 `int3` —— `instr_len` 正是为跨过它而存在的断点 —— 是 trap。后端做归一化,于是 `f->pc += f->instr_len` 在三台机器上都恢复到同一处。另一条路是告诉每一个将要被写出来 的处理函数(包括永远只跑在 RISC 机器上的那些)`pc` 在这里含义不同。 - **接口的一条承诺在这台机器的页表项里无法表达。** riscv 用 `U` 限定 `X`,aarch64 有 独立的 `PXN`/`UXN`,所以"用户映射不可被内核执行"在两者都是编码的属性。x86_64 只有一 个覆盖全部特权级的 `NX`,该规则改由 `CR4.SMEP` 提供,由 `install_memory_attributes()` 设置 —— 与 `MAIR_EL1` 同形的答案,却出于不同的理由。 ## 一个包、两个门面 根 `mcpp.toml` 同时是 `[package]` 与 `[workspace]`。虚拟 workspace 会把接口放进成员目录, `openarch = "0.4.0"` 就得指名它。 消费者写一行依赖,然后二选一:`#include <mcpplibs/openarch.h>` 或 `import mcpplibs.openarch;`。两者是一个库的两种拼写,不是两份互相对齐的声明:模块的 `trap_frame` **就是** `::arch_trap_frame`(`using`,不是同形体),枚举由契约的枚举量 *定义而来* —— `illegal = ARCH_TRAP_ILLEGAL`。`tests/faces.cpp` 检查的是这个推导,而不是 一致性,后者是更弱的东西。 ## 后端由 feature 选择 三个曾由一个机制回答的问题被分开了: - `backend-auto` —— 默认开启,按 target 解析。 - `backend-<arch>` —— 显式指定,为一个 ISA 有多个后端的情形留出位置(riscv 会需要: 这个后端陷入 M 模式,SBI 之下的内核陷入 S 模式)。 - `backend-external` —— **使用者自己实现**。它不指名任何包,而是 *require 能力* `openarch-backend`;图里没有提供者时构建在 configure 阶段就停下并说明,而不是在链接期 报出一个改过名的符号。这与 `std-freestanding` 的分配器同形。 ⚠️ `backend-auto` 刻意**不** require 该能力,而第一版让它 require 了。feature 是可加的, 而 `requires` 是无条件的(哪怕满足它的 `feature-deps` 是 target 条件化的)—— 于是本包 自己的宿主测试无法构建: error: no package provides capability 'openarch-backend' required by 'openarch' 宿主目标没有后端是关于目标的事实,不是消费者能处理的错误。 ## 类型集中到一处 `openarch/types.h` 定义 `arch_u32`/`arch_u64`/`arch_uptr` 并**断言它们的宽度**。此前每处 用点各自拼出 `unsigned long long`,顶上一段注释解释为什么不是 `unsigned long` —— 一条被 描述而从未被检查的规则。它唯一一次被违反(`1UL << 53`)是靠运气发现的:那个移位恰好在 `constexpr` 里,编译器被迫求值。 ## CI ⚠️ **两处修正,而 CI 从 0.3.0 起就是红的,我此前没有去看。** 1. "探针不按架构分支"这条断言太宽:探针必须在恰好一处指名架构 —— 陷入指令,`ebreak` / `brk #0` / `int3` 是同一个想法的三种拼写,没有可移植的第四种。断言收窄为:一个条件块, 块内除指令外别无他物。 2. portability 作业在仓库根跑 `mcpp build --target riscv64-none-elf`,而 0.3.1 的根是虚拟 workspace —— 于是它对**所有成员**扇出,把 aarch64 汇编喂给 riscv 汇编器 (`unrecognized instruction mnemonic, did you mean: sra, srl?`)。混合式的根修好了它: 根现在是接口包,构建它只拉入该 target 的后端。 x86_64 一行的模拟器来自 apt 并注明了原因:xPack 按目标族发布 QEMU 而没有 x86 构建, 所以生态里没有 `xim:qemu-x86` 可装。 * 模板:一份源码、三台机器的裸机起点 `mcpp new mykernel --template openarch` 生成的工程就是探针本身:一份 src/main.cpp、三个带控制台与断电寄存器的 machine_<arch>.cpp、三份链接脚本, 以及只有 x86_64 才需要的那一百来条到达长模式的指令。 ⚠️ CI 用 sed 手工渲染模板而不是走 `mcpp new`。scaffolder 从**索引**解析 --template 且不接受路径,所以为一个「本次提交才加进来的模板」调用它,解析到的 是上一个已发布版本,而那个版本不带这个模板 —— 新模板的 CI 永远不可能在它被发布 之前通过。属于本仓库的是模板的**内容**。 ⚠️ 只在 Linux 上跑:那一步把 $PWD 写进清单,而 Windows runner 的 Git Bash 下 $PWD 是 /d/a/openarch/openarch,mcpp 要的是原生路径 —— 本生态已经被这个形状咬过 一次(一个 C++ 字符串字面量里出现了 "D:\a\openkal\openkal/include")。
1 parent 1cc6a14 commit 6a34941

51 files changed

Lines changed: 3348 additions & 321 deletions

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: 171 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@ on:
66
pull_request:
77
workflow_dispatch:
88

9-
# Three jobs, and the first two are ONE job written once: the gate this layer is
10-
# judged by is that a single probe source runs on two genuinely different
11-
# machines, so the two rows below differ only in a triple and an emulator
12-
# package. If they ever need to differ in anything else, the abstraction has
13-
# failed and this workflow is where that becomes visible.
9+
# Three jobs, and the first is ONE job written once: the gate this layer is
10+
# judged by is that a single probe source runs on machines that genuinely
11+
# differ, so the rows below differ only in a triple and an emulator. If they
12+
# ever need to differ in anything else, the abstraction has failed and this
13+
# workflow is where that becomes visible.
14+
#
15+
# ⭐ THE THIRD ROW IS WHAT TURNS THE GATE INTO EVIDENCE. riscv64 and aarch64 are
16+
# both load/store RISC machines with a weak memory model and a fixed instruction
17+
# width, so an interface that fits both may fit because it is right or because
18+
# they are alike. x86_64 is neither: variable-length instructions, total store
19+
# order — under which three of the four barriers need no instruction at all —
20+
# and an interrupt mechanism that is a table of 256 gates rather than a base
21+
# register. What survives all three is an abstraction.
1422
jobs:
1523
gate:
1624
name: the probe runs on ${{ matrix.arch }}
@@ -20,10 +28,23 @@ jobs:
2028
fail-fast: false
2129
matrix:
2230
include:
23-
- { arch: riscv64, triple: riscv64-none-elf, qemu: 'xim:qemu-riscv' }
24-
- { arch: aarch64, triple: aarch64-none-elf, qemu: 'xim:qemu-arm' }
31+
- { arch: riscv64, triple: riscv64-none-elf, qemu: 'xim:qemu-riscv', apt: '' }
32+
- { arch: aarch64, triple: aarch64-none-elf, qemu: 'xim:qemu-arm', apt: '' }
33+
# ⚠️ THE THIRD ROW'S EMULATOR COMES FROM apt, AND THAT IS A GAP IN THE
34+
# ECOSYSTEM RATHER THAN A PREFERENCE.
35+
#
36+
# The other two emulators are xlings packages because xPack publishes
37+
# QEMU per target family and the index carries what it publishes.
38+
# xPack has no x86 build, so there is no `xim:qemu-x86` to install —
39+
# see .agents/docs/2026-08-21-freestanding-outstanding-four.md §4 in
40+
# the engine repository, where building one is staged work.
41+
#
42+
# Naming apt here rather than quietly relying on whatever is on the
43+
# runner keeps the difference visible: this row is the one whose
44+
# emulator the ecosystem does not yet own.
45+
- { arch: x86_64, triple: x86_64-none-elf, qemu: '', apt: 'qemu-system-x86' }
2546
env:
26-
MCPP_VERSION: 2026.8.20.3
47+
MCPP_VERSION: 2026.8.21.1
2748
XLINGS_VERSION: v2026.8.17.2
2849
XLINGS_NON_INTERACTIVE: '1'
2950
steps:
@@ -94,18 +115,25 @@ jobs:
94115
# `qemu-arm` only the two Arm ones. Measured; no single package runs both.
95116
- name: Install the emulator
96117
run: |
97-
xlings install ${{ matrix.qemu }} -y
98-
XLINGS_HOME="$HOME/.mcpp/registry" xlings install ${{ matrix.qemu }} -y
118+
if [ -n "${{ matrix.qemu }}" ]; then
119+
xlings install ${{ matrix.qemu }} -y
120+
XLINGS_HOME="$HOME/.mcpp/registry" xlings install ${{ matrix.qemu }} -y
121+
else
122+
sudo apt-get update -qq
123+
sudo apt-get install -y -qq ${{ matrix.apt }}
124+
qemu-system-x86_64 --version | head -1
125+
fi
99126
100127
- name: The layer builds for ${{ matrix.arch }}
101128
run: |
102129
# Twice, the first allowed to fail: the toolchain payload is installed
103130
# during a build, so the first build on a machine that has never
104131
# targeted this triple is the one that installs it.
105-
# ⚠️ THE BACKEND, NOT THE WORKSPACE ROOT. Since 0.3.1 the root is a
106-
# workspace and carries no sources of its own; building the backend
107-
# for this target pulls the ABI it implements and is what the probe
108-
# below will link against.
132+
# ⚠️ THE BACKEND, NOT THE ROOT. Since 0.4.0 the root is BOTH the
133+
# interface package and the workspace, so building it would build the
134+
# interface; what this step wants is the machine half. Building the
135+
# backend for this target pulls the ABI it implements and is what the
136+
# probe below will link against.
109137
( cd backends/${{ matrix.arch }} && mcpp build --target ${{ matrix.triple }} ) || true
110138
( cd backends/${{ matrix.arch }} && mcpp build --target ${{ matrix.triple }} )
111139
@@ -120,10 +148,26 @@ jobs:
120148
working-directory: examples/switch
121149
run: |
122150
set -euo pipefail
123-
mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log
151+
if [ -n "${{ matrix.qemu }}" ]; then
152+
mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log
153+
else
154+
# ⚠️ `mcpp build` AND THEN qemu BY HAND, BECAUSE `build.mcpp`
155+
# CONFIGURES NO RUNNER FOR THIS TARGET. It asks mcpp where
156+
# `xim:qemu-x86` landed and there is no such package; a build
157+
# program that fell back to a bare `qemu-system-x86_64` would make
158+
# the build depend on what happens to be installed on the machine,
159+
# which is the thing the other two rows do not do.
160+
mcpp build --target ${{ matrix.triple }}
161+
IMG=$(find target/${{ matrix.triple }} -type f -name switch | head -1)
162+
test -n "$IMG"
163+
timeout -k 5 60 qemu-system-x86_64 -machine q35 -nographic -no-reboot \
164+
-kernel "$IMG" 2>&1 | tee run.log
165+
fi
124166
grep -q "task: arg=42" run.log
125167
grep -q "witness=7 before=1234" run.log
126168
grep -q "switch ok" run.log
169+
grep -q "trap: back, witness=1" run.log
170+
grep -q "cpu: percpu round-trips" run.log
127171
128172
# The same source produced that output. Asserted rather than trusted: a
129173
# probe that had quietly grown a per-architecture branch would still pass
@@ -132,26 +176,73 @@ jobs:
132176
run: |
133177
set -euo pipefail
134178
test -f examples/switch/src/main.cpp
135-
if grep -qE '__riscv|__aarch64__|MCPP_TARGET_ARCH' examples/switch/src/main.cpp; then
136-
echo "the probe branches on the architecture, which is what it exists to avoid"
179+
180+
# ⚠️ THIS CHECK USED TO READ "NO ARCHITECTURE MACRO APPEARS AT ALL",
181+
# AND IT WAS WRONG IN A WAY THAT COST TWO RELEASES OF RED CI.
182+
#
183+
# The probe must name an architecture in exactly one place: the trap
184+
# instruction. `ebreak`, `brk #0` and `int3` are three spellings of
185+
# one idea and there is no portable fourth. When the trap interface
186+
# landed in 0.3.0 the old check began failing on every push, correctly
187+
# by its own wording and wrongly by its intent — and it kept failing
188+
# through 0.3.1 because nobody read the runs.
189+
#
190+
# What the gate actually claims is that the probe is not TWO PROGRAMS.
191+
# So: one conditional, and nothing inside it but instructions. A
192+
# second conditional, or a line of logic inside the first, is the
193+
# thing this is here to catch.
194+
NCOND=$(grep -c '^#if defined(__' examples/switch/src/main.cpp || true)
195+
if [ "$NCOND" != "1" ]; then
196+
grep -n '^#if defined(__' examples/switch/src/main.cpp || true
197+
echo "the probe has $NCOND architecture conditionals; it may have exactly one, for the trap instruction"
137198
exit 1
138199
fi
200+
STRAY=$(awk '
201+
/^#if defined\(__/ || /^#elif defined\(__/ { inb=1; next }
202+
/^#else/ { next }
203+
/^#endif/ { inb=0; next }
204+
inb && $0 !~ /^[[:space:]]*(\/\/)?[[:space:]]*$/ \
205+
&& $0 !~ /asm volatile/ && $0 !~ /^#[[:space:]]*error/ \
206+
&& $0 !~ /^[[:space:]]*\/\// { print NR": "$0 }
207+
' examples/switch/src/main.cpp)
208+
if [ -n "$STRAY" ]; then
209+
echo "$STRAY"
210+
echo "the architecture conditional contains something other than an instruction"
211+
exit 1
212+
fi
213+
echo "the probe names an architecture once, to emit a trap instruction"
139214
140215
# ⚠️ THE SEPARATION IS ASSERTED, NOT TRUSTED TO THE DIRECTORY NAMES.
141216
#
142217
# Until 0.3.1 the backends lived under `src/arch/<arch>/` in the same
143218
# package as the specification, and the layering was a convention held
144219
# up by a path. The split made it a dependency graph; these two checks
145220
# make it a test.
146-
if grep -rqE '\basm\b|__asm' spec/src; then
147-
echo "the specification contains an instruction; it must not"
221+
if grep -rqE '\basm\b|__asm' src; then
222+
echo "the interface contains an instruction; it must not"
148223
exit 1
149224
fi
150225
if grep -rq 'export module' backends; then
151-
echo "a backend exports a module; it must reach the specification only through the ABI"
226+
echo "a backend exports a module; it must reach the interface only through the ABI"
152227
exit 1
153228
fi
154-
echo "spec owns modules and no instruction; backends own instructions and no module"
229+
echo "the interface owns modules and no instruction; backends own instructions and no module"
230+
231+
# ⚠️ THE ROOT IS BOTH A PACKAGE AND A WORKSPACE, AND THAT IS WHAT
232+
# MAKES A CONSUMER'S SIDE ONE LINE. A virtual workspace would put the
233+
# interface in a member directory and `openarch = "0.4.0"` would have
234+
# to name it. Asserted because the two tables are ordinary TOML and
235+
# deleting one would leave a manifest that still builds.
236+
grep -q '^\[package\]' mcpp.toml
237+
grep -q '^\[workspace\]' mcpp.toml
238+
239+
# The two faces reach the same library. The C one is a header a
240+
# consumer includes; the C++ one is a module it imports. A face that
241+
# disappeared would not fail any build in this repository except the
242+
# test that names it.
243+
test -f abi/include/mcpplibs/openarch.h
244+
grep -q 'export module mcpplibs.openarch;' src/openarch.cppm
245+
echo "one package, two faces, three backends"
155246
156247
# ---------------------------------------------------------------------------
157248
# The half no emulator can check.
@@ -173,7 +264,7 @@ jobs:
173264
run:
174265
shell: bash
175266
env:
176-
MCPP_VERSION: 2026.8.20.3
267+
MCPP_VERSION: 2026.8.21.1
177268
XLINGS_VERSION: v2026.8.17.2
178269
XLINGS_NON_INTERACTIVE: '1'
179270
steps:
@@ -218,8 +309,63 @@ jobs:
218309
# header now: a target build instantiates only what it calls — measured,
219310
# zero foreign symbols in either image — and a host build that calls both
220311
# gets both, with nothing to activate.
221-
- name: Both encoders compile here and agree
222-
working-directory: spec
312+
# ⚠️ THE TEMPLATE IS RENDERED BY HAND HERE, AND IT HAS TO BE.
313+
#
314+
# `mcpp new --template` resolves the package from the INDEX and takes no
315+
# path, so asking it for a template this commit ADDS would resolve the
316+
# previously published version and fail on a template that version does
317+
# not carry. The CI for a new template could never pass before the
318+
# template was published, which is the wrong way round.
319+
#
320+
# What belongs to this repository is the template's CONTENT: that the
321+
# files it ships generate a project which builds for all three machines.
322+
# That the scaffolder can fetch it is mcpp's own concern and mcpp's own
323+
# tests.
324+
#
325+
# ⚠️ The dependency is rewritten to a path. The rendered manifest names
326+
# `openarch = "<this version>"`, which is correct for a user and
327+
# unresolvable here — this version is not published yet, and a check that
328+
# silently fell back to the previous one would be testing the wrong tree.
329+
#
330+
# ⚠️ LINUX ONLY, AND THAT IS A CHOICE ABOUT PATHS RATHER THAN ABOUT
331+
# COVERAGE. The rewrite below puts `$PWD` into a manifest, and on a
332+
# Windows runner `$PWD` under Git Bash is `/d/a/openarch/openarch` while
333+
# mcpp wants a native path — a mismatch this repository has already been
334+
# bitten by once, in a C++ string literal that came out as
335+
# `"D:\a\openkal\openkal/include"`. The template's CONTENT is
336+
# host-independent; what varies per host is the toolchain payload, and
337+
# that is what the `portability` job covers.
338+
- name: The template generates a project that builds for all three machines
339+
if: runner.os == 'Linux'
340+
run: |
341+
set -euo pipefail
342+
T=templates/three-machines
343+
V=$(grep -m1 '^version' mcpp.toml | cut -d'"' -f2)
344+
D=$(mktemp -d); mkdir -p "$D/src"
345+
for f in mcpp.toml README.md build.mcpp; do
346+
sed -e "s/{{project\.name}}/k/g" -e "s/{{self\.version}}/$V/g" \
347+
"$T/$f.in" > "$D/$f"
348+
done
349+
cp "$T"/*.ld "$D/"
350+
cp "$T"/src/* "$D/src/"
351+
sed -i.bak "s|openarch = \"$V\"|openarch = { path = \"$PWD\" }|" "$D/mcpp.toml"
352+
rm -f "$D/mcpp.toml.bak"
353+
grep -q "path = " "$D/mcpp.toml" || { cat "$D/mcpp.toml"; echo "the dependency rewrite did not apply"; exit 1; }
354+
for t in riscv64-none-elf aarch64-none-elf x86_64-none-elf; do
355+
( cd "$D" && mcpp build --target "$t" > /dev/null 2>&1 ) || true
356+
( cd "$D" && mcpp build --target "$t" ) \
357+
|| { echo "the template does not build for $t"; exit 1; }
358+
done
359+
echo "the template builds for riscv64, aarch64 and x86_64"
360+
361+
# ⚠️ THREE ENCODERS SINCE 0.4.0, AND THE THIRD SETTLED A QUESTION THE
362+
# FIRST TWO LEFT OPEN. `openarch.pte` owns `MAIR_EL1` because aarch64's
363+
# entry holds an INDEX into it rather than a memory type, while riscv's
364+
# holds the type itself — one against one, and "this layer owns the
365+
# attribute register" could fairly be called a workaround for aarch64.
366+
# x86_64 does the same thing with `IA32_PAT`, on a machine that shares no
367+
# lineage with it, so the majority is now two to one the other way.
368+
- name: The encoders compile here and agree
223369
run: mcpp test
224370

225371
# ---------------------------------------------------------------------------
@@ -257,7 +403,7 @@ jobs:
257403
run:
258404
shell: bash
259405
env:
260-
MCPP_VERSION: 2026.8.20.3
406+
MCPP_VERSION: 2026.8.21.1
261407
XLINGS_VERSION: v2026.8.17.2
262408
XLINGS_NON_INTERACTIVE: '1'
263409
steps:

0 commit comments

Comments
 (0)