From cf27e9393a7333e748b5cffa2a4186255d2d84d1 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 21:37:53 +0800 Subject: [PATCH] Assert the positions __config_site declares, and repin the C library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `__config_site` is this package's claim about the environment beneath, and a claim that drifts from what the port provides fails neither the build nor the link: it produces a program that takes a path the environment cannot support and reports nothing. `_LIBCPP_HAS_TERMINAL' is the position where that already happened once. The hosted configuration declares FILESYSTEM, RANDOM_DEVICE and TERMINAL and builds the sources behind each, and the bare-metal one declares none of the three and excludes them --- so the exclusions and the switches agree. Nothing in examples/cxx exercised any of them. It does now, and every observation is written so that it can fail: std::filesystem a directory created, a file written, enumerated, copied, its size reported, the whole removed permissions and ⭐⭐ REFUSED, NOT IGNORED. `kal_node_info' carries a create_symlink boolean `writable' rather than a mode word, and SURFACE.txt has no operation that creates a link. A probe checking only the supported operations would pass just as well for a port that silently accepted these two --- which is what openkal-linux#13 described as "expected 0600, got 0777". std::random_device three draws that DIFFER. "A number was produced" is satisfied by a source stuck at a constant. hidden, weak, the C++ half of openkal-musl#13. In C++ `hidden' was weak_alias given C LINKAGE rather than emptied, so it failed differently from the C case and would not have been caught by it. The report came from a C++23 workspace. Measured here: 17 observations held, none failed. openkal-musl moves to 0.5.0, which is where the routes those refusals are the other side of now live. Also translates the outline-atomics and function-multi-versioning notes in the manifest into English, which is what every other comment in this ecosystem is written in. --- .github/workflows/ci.yml | 12 +++++- .gitignore | 4 ++ README.md | 38 +++++++++++++---- examples/cxx/src/main.cpp | 90 +++++++++++++++++++++++++++++++++++++++ mcpp.toml | 70 +++++++++++++++++------------- 5 files changed, 174 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 872bb982..c4507069 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,10 +100,20 @@ jobs: # _Unwind_Backtrace walking zero frames --- and nothing named the cause. # So the throw is the check, and the destructor beside it is what says the # unwind was correct rather than merely non-fatal. - - name: A C++ program above it throws across frames and catches + # ⭐ THE UNWINDER, AND EVERY POSITION `__config_site` DECLARES. + # + # The throw and the destructor beside it are what say the runtime is + # really there. The rest of the probe asks whether the CLAIMS this package + # makes about the environment beneath are true: `std::filesystem` over + # openkal.fs, `std::random_device` over openkal.random, and the two + # operations openkal has no atom for --- which must be REFUSED rather than + # ignored, and which a probe checking only the supported ones would pass + # for a port that silently accepted them. + - name: A C++ program above it throws, catches, and uses what is declared run: | cd examples/cxx && mcpp run 2>&1 | tee out.log grep -q -- '-- failures: 0 --' out.log + ! grep -q '^FAIL:' out.log - name: import std above it run: | diff --git a/.gitignore b/.gitignore index 569cbfe3..b4f8ce20 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ mcpp.lock .spec/ .impl/ .openkal-*/ + +# What examples/cxx writes while it runs, if it stops before removing it. +cxx-probe.d/ +out.log diff --git a/README.md b/README.md index 2653fe8e..ace6499f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ C library. ```toml [dependencies] -openkal-llvm-runtime = "0.1.0" +openkal-llvm-runtime = "0.3.0" ``` A C++ standard library is not portable in the way a program is. It is @@ -31,13 +31,14 @@ itself builds separately from the compiler. ## The one thing the configuration decides -`llvm-generated/generic/__config_site` is libc++'s configure product. Two -positions in it carry the whole of what makes this package different from the -one a toolchain ships: +`llvm-generated/generic/__config_site` is libc++'s configure product, and every +position in it is a **claim about the environment beneath**: ```c -#define _LIBCPP_HAS_MUSL_LIBC 1 /* the C library beneath is musl's */ -#define _LIBCPP_HAS_RANDOM_DEVICE 0 /* openkal has no source of entropy */ +#define _LIBCPP_HAS_MUSL_LIBC 1 /* the C library beneath is musl's */ +#define _LIBCPP_HAS_RANDOM_DEVICE 1 /* openkal.random reaches a source */ +#define _LIBCPP_HAS_FILESYSTEM 1 /* openkal.fs, and its sources built */ +#define _LIBCPP_HAS_TERMINAL 1 /* isatty answers, rather than lying */ ``` The first was measured rather than assumed. With it at `0` — the value a @@ -49,9 +50,17 @@ __locale:439: error: unknown rune table for this platform -- do you mean to define _LIBCPP_PROVIDES_DEFAULT_RUNE_TABLE? ``` -With it at `1`, none. The second follows from openkal reporting `ENOSYS` for -entropy: `std::random_device` is not built, and a program that names it is told -by the linker. +With it at `1`, none. + +⚠️ **A claim that drifts from what the port provides fails neither the build nor +the link.** It produces a program that takes a path the environment cannot +support, and reports nothing. `_LIBCPP_HAS_RANDOM_DEVICE` was `0` until openkal +gained `openkal.random`; `_LIBCPP_HAS_TERMINAL` was `1` while every `isatty` +over the port beneath returned `0` — for a real terminal as readily as for a +pipe — so `std::print` never took its terminal path and nothing failed. The +remedy there was to repair the port rather than withdraw the claim, and the +workflow now reconciles the two rather than checking once and assuming +afterwards. ## Two configurations, and why there are two @@ -95,6 +104,17 @@ whether a hosted standard library is *present*. - values returned through several frames - **an exception thrown across three frames and caught** - **a destructor run while the stack is unwound** +- `hidden`, `weak` and `weak_alias` as the program's own identifiers — the C++ + half of `mcpplibs/openkal-musl#13`, where `hidden` was given C *linkage* + rather than emptied and so failed differently from the C case +- `std::filesystem` — a directory created, a file written, enumerated, copied, + its size reported, and the whole removed +- **`std::filesystem::permissions` and `create_symlink` refused, not ignored** — + openkal carries a boolean `writable` rather than a mode word, and has no + operation that creates a link. A probe checking only the supported operations + would pass just as well for a port that silently accepted these two +- `std::random_device` — three draws that **differ**, which a source stuck at a + constant would not satisfy and "a number was produced" would `examples/import-std` asserts the other half: `import std;` — the module, not the headers — with `std::ranges::sort` and `std::println`. diff --git a/examples/cxx/src/main.cpp b/examples/cxx/src/main.cpp index 277ecfa1..ee2b92ab 100644 --- a/examples/cxx/src/main.cpp +++ b/examples/cxx/src/main.cpp @@ -1,11 +1,36 @@ // What a C++ program above openkal reaches for, and the one observation that // settles whether the runtime is really there: an exception thrown across a // frame and caught. +// +// ⭐ AND THE PARTS OF THE STANDARD LIBRARY WHOSE AVAILABILITY IS A STATEMENT +// ABOUT THE ENVIRONMENT BENEATH. `__config_site` declares +// `_LIBCPP_HAS_FILESYSTEM 1` and `_LIBCPP_HAS_RANDOM_DEVICE 1` for a hosted +// row; those declarations are this package's claim about openkal-musl, and a +// claim that drifts from what the port provides does not fail to build and does +// not fail to link --- it produces a program that takes a path the environment +// cannot support and reports nothing. The same shape as `_LIBCPP_HAS_TERMINAL`, +// which is what the workflow's reconciliation step exists because of. #include #include #include #include #include +#include +#include +#include + +// ⭐ THREE NAMES A PROGRAM ABOVE THIS STACK MAY USE, ASSERTED BY COMPILING. +// +// musl's INTERNAL header overlay defines `hidden`, `weak` and `weak_alias` as +// macros that mean something only to musl's own sources, and openkal-musl used +// to publish the path it is built from --- so a program above it could not +// declare any of the three (mcpplibs/openkal-musl#13). This is the C++ half of +// that criterion, and it belongs here rather than there: the report came from a +// C++23 workspace, and `hidden` in C++ was given C LINKAGE rather than emptied, +// which is a different failure from the C one and would not be caught by it. +static int hidden = 7; +static int weak = 11; +struct weak_alias { int value; }; static int depth_three(int n) { if (n > 2) throw std::runtime_error("thrown"); return n; } static int depth_two(int n) { return depth_three(n) + 1; } @@ -39,6 +64,71 @@ int main() { try { sentinel g{&destroyed}; throw 1; } catch (int) {} check(destroyed, "a destructor runs while the stack is unwound"); + check(hidden + weak == 18 && weak_alias{3}.value == 3, + "hidden, weak and weak_alias are the program's own identifiers"); + + // --- std::filesystem, which is the C++ face of openkal.fs --------------- + + namespace fs = std::filesystem; + const fs::path dir = "cxx-probe.d"; + std::error_code ec; + + fs::remove_all(dir, ec); + check(fs::create_directory(dir, ec) && !ec, "a directory is created"); + + { + std::FILE* f = std::fopen((dir / "a.txt").c_str(), "w"); + check(f != nullptr, "a file is created inside it"); + if (f) { std::fputs("0123456789", f); std::fclose(f); } + } + + check(fs::exists(dir / "a.txt", ec) && !ec, "the file is found by name"); + check(fs::file_size(dir / "a.txt", ec) == 10 && !ec, "its size is reported"); + + int entries = 0; + for (const auto& e : fs::directory_iterator(dir, ec)) { (void)e; ++entries; } + check(entries == 1 && !ec, "the directory enumerates exactly what is in it"); + + check(fs::copy_file(dir / "a.txt", dir / "b.txt", ec) && !ec, + "a file is copied"); + check(fs::file_size(dir / "b.txt", ec) == 10 && !ec, + "and the copy has the same size"); + + // ⭐⭐ AND THE TWO OPERATIONS openkal HAS NO ATOM FOR, CHECKED AS REFUSALS. + // + // `kal_node_info` carries a boolean `writable` and not a mode word, and + // SURFACE.txt has no operation that creates a symbolic link. openkal-musl + // therefore refuses `chmod` and `symlink` rather than succeeding and + // reporting something else afterwards --- and a refusal that arrives as a + // `std::error_code` is what a C++ caller can act upon. + // + // ⚠️ THIS IS THE HALF THAT WOULD BE OMITTED. A probe checking only that the + // supported operations work would pass just as well for a port that + // silently accepted these two, which is the outcome the report + // (openkal-linux#13) described as "expected 0600, got 0777". + ec.clear(); + fs::permissions(dir / "a.txt", fs::perms::owner_read, ec); + check(static_cast(ec), "changing permission bits is refused, not ignored"); + + ec.clear(); + fs::create_symlink(dir / "a.txt", dir / "link", ec); + check(static_cast(ec), "creating a symbolic link is refused, not ignored"); + + fs::remove_all(dir, ec); + check(!fs::exists(dir, ec), "the directory and its contents are removed"); + + // --- std::random_device, which is the C++ face of openkal.random -------- + + { + std::random_device rd; + const unsigned a = rd(), b = rd(), c = rd(); + // ⚠️ THE CRITERION IS THAT THEY DIFFER, NOT THAT ANY ONE OF THEM IS + // ANYTHING. A source stuck at a constant satisfies "a number was + // produced" and is exactly what a port that forgot to fill the buffer + // would produce. + check(!(a == b && b == c), "three draws from the entropy source differ"); + } + std::printf("-- failures: %d --\n", failures); return failures != 0; } diff --git a/mcpp.toml b/mcpp.toml index 465702a3..ff0e1a38 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-llvm-runtime" -version = "0.2.0" +version = "0.3.0" description = "LLVM's C++ runtime libraries — libc++, libc++abi and libunwind — configured for openkal-musl rather than for a host C library." license = "Apache-2.0" authors = ["mcpplibs"] @@ -147,58 +147,68 @@ std-module-flags = [ # criterion the whole package exists to satisfy is the one openkal keeps # returning to --- whether an implementation has been configured FOR this # target, not whether its headers can be found. -# ⭐⭐ outline-atomics 的 125 个辅助函数,由本包供给 —— 因为本包就是那份 -# compiler-rt。 +# ⭐⭐ THE 125 OUTLINE-ATOMICS HELPERS ARE SUPPLIED BY THIS PACKAGE, BECAUSE +# THIS PACKAGE IS THAT compiler-rt. # -# `--rtlib=compiler-rt` 让 clang 在 aarch64 上开启 `+outline-atomics`, -# 而它是对的:该特性的 `__aarch64_*` 辅助函数本来就住在 compiler-rt 里。 -# 此前本包不产出它们,于是特性被开启而没有人实现: +# `--rtlib=compiler-rt` makes clang enable `+outline-atomics` on aarch64, and it +# is right to: the feature's `__aarch64_*` helpers live in compiler-rt. This +# package did not produce them, so the feature was enabled and nobody +# implemented it: # # ld.lld: error: undefined symbol: __aarch64_swp4_acq # ld.lld: error: undefined symbol: __aarch64_cas8_acq_rel # -# ⚠️ 引用来自 `openkal-linux/src/memory.o` 与 `openkal-musl/port/src/okm_fd.o` -# —— 两个对这件事一无所知、只是用了原子操作的包。缺的是这份运行时该有的东西。 +# ⚠️ The references came from `openkal-linux/src/memory.o` and +# `openkal-musl/port/src/okm_fd.o` --- two packages that know nothing about any +# of this and merely used an atomic operation. What was missing was something +# this runtime is supposed to carry. # -# upstream 的做法是把 `aarch64/lse.S` 编 125 遍,每遍给不同的 -# `-DL_ -DSIZE= -DMODEL=`(6 模式 × 5 尺寸 × 5 模型, -# 非 cas 的 16 字节档不存在)。一个用 glob 表达 sources 的清单传不了 -# per-file 定义 —— 于是每个组合成为一个**自己声明宏、再 include 共享正文** -# 的文件。正文是 upstream 的,未经修改,仍在原处。 +# Upstream compiles `aarch64/lse.S` 125 times, each with a different +# `-DL_ -DSIZE= -DMODEL=` (6 patterns x 5 sizes x 5 models; the +# 16-byte width exists only for `cas`). A manifest that states its sources as a +# glob cannot carry a per-file definition --- so each combination becomes a file +# that DECLARES THE MACROS ITSELF AND THEN INCLUDES THE SHARED BODY. The body is +# upstream's, unmodified, and stays where it is. # -# ⭐ 这是把宏从**命令行**移到**文件内部**,而不是复制 125 份实现。 +# ⭐ That moves the macros from the COMMAND LINE into the FILE, rather than +# copying the implementation 125 times. [target.'cfg(arch = "aarch64")'.build] sources = [ "llvm-generated/outline-atomics/*.S", - # ⚠️ 并且定义那个探测变量的文件。这 125 个辅助函数每一个都读 - # `__aarch64_have_lse_atomics` —— 它在运行时决定走 LSE 指令还是 - # load/store-exclusive 循环。少了它: + # ⚠️ And the file that defines the probe. Every one of the 125 helpers reads + # `__aarch64_have_lse_atomics`, which decides at run time between the LSE + # instructions and a load/store-exclusive loop. Without it: # # ld.lld: error: undefined hidden symbol: __aarch64_have_lse_atomics # - # ⭐ 它在 `cpu_model/aarch64.c`,而 `[build]` 段的 glob 是 - # `llvm/compiler-rt/lib/builtins/*.c` —— **不含子目录**,所以这个文件 - # 从未被编过。upstream 把它列在 `aarch64_SOURCES` 里,与本节同一批。 + # ⭐ It is in `cpu_model/aarch64.c`, and the `[build]` section's glob is + # `llvm/compiler-rt/lib/builtins/*.c` --- WHICH DOES NOT DESCEND INTO + # SUBDIRECTORIES, so that file had never been compiled. Upstream lists it in + # `aarch64_SOURCES`, in the same set as this section. "llvm/compiler-rt/lib/builtins/cpu_model/aarch64.c", ] -# ⚠️ FMV 关掉,而它不是可选的清理 —— 那半个文件在 macOS 宿主上根本编不过。 +# ⚠️ FUNCTION MULTI-VERSIONING IS TURNED OFF, AND THAT IS NOT OPTIONAL TIDYING: +# that half of the file does not compile on a macOS host at all. # -# `cpu_model/aarch64.c` 的后半是函数多版本(FMV)的实现,按宿主分支 include -# 一个 `.inc`。Apple 那支拉的是系统头: +# The second half of `cpu_model/aarch64.c` implements function multi-versioning, +# and includes one `.inc` per host. Apple's branch reaches for a system header: # # aarch64/fmv/apple.inc:1:10: fatal error: 'TargetConditionals.h' file not found # -# 而本包的 `include_dirs` 刻意不含任何来自机器的路径 —— 见该段的 -# ⭐⭐ NOTHING FROM THE MACHINE。⚠️ 本机 Linux 上编得过,CI 的 macOS 那格才炸, -# 是典型的「一台宿主看不见」的形状。 +# and this package's `include_dirs` deliberately contains no path that comes +# from the machine --- see ⭐⭐ NOTHING FROM THE MACHINE in that section. +# ⚠️ It compiles on this Linux machine and fails only on the macOS row of +# continuous integration, which is the shape one host cannot see. # -# ⭐ 而本包**不需要** FMV:要的只是 `__aarch64_have_lse_atomics`, -# 它定义在 `#if !defined(DISABLE_AARCH64_FMV)` 之外。upstream 自己也有这个 -# 开关(`COMPILER_RT_DISABLE_AARCH64_FMV`),所以这是走它给的门,不是绕过。 +# ⭐ And this package does NOT need multi-versioning: what it wants is +# `__aarch64_have_lse_atomics`, which is defined outside +# `#if !defined(DISABLE_AARCH64_FMV)`. Upstream has the same switch +# (`COMPILER_RT_DISABLE_AARCH64_FMV`), so this goes through the door upstream +# provides rather than around it. cflags = ["-DDISABLE_AARCH64_FMV=1"] [dependencies] -openkal-musl = "0.4.0" +openkal-musl = "0.5.0" [build] cxx_standard = "c++23"