Skip to content

feat(freestanding): exceptions off graph-wide, and the std subset becomes usable - #459

Open
Sunrisepeak wants to merge 2 commits into
mainfrom
feat/freestanding-std-subset
Open

feat(freestanding): exceptions off graph-wide, and the std subset becomes usable#459
Sunrisepeak wants to merge 2 commits into
mainfrom
feat/freestanding-std-subset

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

The freestanding std subset was described in the phase-3 wrap-up as "a separate
piece of work". That was wrong, and the research it contradicted had already
measured it. Reproduced here on the shipped payloads: with a synthesised
__config_site, 103 of libc++'s 110 headers compile for riscv64-none-elf, and
all 7 that fail also fail on an x86_64 host with full libc++ and glibc — they
are headers libc++ has not implemented. The freestanding compile-time loss is
zero. -nostdinc++ is the MECHANISM of that route, not an obstacle: it keeps
libc++'s headers private to the subset package, which exports a module — the
same shape the board package already uses for the target's C library.

What actually blocked it was exceptions, and that is a whole-graph property:

  • std::optional::value() alone pulls in __cxa_throw, two vtables and
    std::bad_optional_access::~bad_optional_access, none of which can exist
    without an unwinder or libc++abi.
  • A project's own [build] cxxflags cannot fix it: they do not reach a
    dependency's module compile, so the BMI and the importer disagree and clang
    reports a .pcm "configuration mismatch" — naming a module file rather
    than the setting that split the graph.
  • is_dialect_flag deliberately does NOT propagate -fno-exceptions, on the
    grounds that "dependencies may assume exceptions are available". Right for
    a hosted target, exactly backwards here.

So -fno-exceptions -fno-rtti join -ffreestanding and -nostdinc++ on the
target spec, which is the one place that reaches every unit in the graph.

⚠️ That change alone would have broken every existing bare-metal user on
upgrade. The dependency cache key carries the target TRIPLE but not the flags
the triple implies — and which flags those are is mcpp's decision, so it moves
between versions while the triple string does not. A project that had built
once got its old BMI back and a hard failure naming a .pcm. Fixed by adding a
targetImpliedFlags axis, empty for hosted targets so no existing key moves.

e2e/133 pins the whole chain in the emulator, from both sides: the subset
builds, runs ranges::sort with a projection plus optional/atomic/span/
string_view on the target, and std::mutex fails to COMPILE — capabilities
turned off in __config_site vanish rather than leaving a run-time stub.

The import std diagnostic names std-freestanding again, now that the
package is published.

Ships as 2026.8.19.4, alongside mcpplibs/std-freestanding 0.1.0.

…omes usable

The freestanding std subset was described in the phase-3 wrap-up as "a separate
piece of work". That was wrong, and the research it contradicted had already
measured it. Reproduced here on the shipped payloads: with a synthesised
`__config_site`, 103 of libc++'s 110 headers compile for riscv64-none-elf, and
all 7 that fail also fail on an x86_64 host with full libc++ and glibc — they
are headers libc++ has not implemented. The freestanding compile-time loss is
zero. `-nostdinc++` is the MECHANISM of that route, not an obstacle: it keeps
libc++'s headers private to the subset package, which exports a module — the
same shape the board package already uses for the target's C library.

What actually blocked it was exceptions, and that is a whole-graph property:

  * `std::optional::value()` alone pulls in `__cxa_throw`, two vtables and
    `std::bad_optional_access::~bad_optional_access`, none of which can exist
    without an unwinder or libc++abi.
  * A project's own `[build] cxxflags` cannot fix it: they do not reach a
    dependency's module compile, so the BMI and the importer disagree and clang
    reports a `.pcm` "configuration mismatch" — naming a module file rather
    than the setting that split the graph.
  * `is_dialect_flag` deliberately does NOT propagate `-fno-exceptions`, on the
    grounds that "dependencies may assume exceptions are available". Right for
    a hosted target, exactly backwards here.

So `-fno-exceptions -fno-rtti` join `-ffreestanding` and `-nostdinc++` on the
target spec, which is the one place that reaches every unit in the graph.

⚠️ That change alone would have broken every existing bare-metal user on
upgrade. The dependency cache key carries the target TRIPLE but not the flags
the triple implies — and which flags those are is mcpp's decision, so it moves
between versions while the triple string does not. A project that had built
once got its old BMI back and a hard failure naming a .pcm. Fixed by adding a
`targetImpliedFlags` axis, empty for hosted targets so no existing key moves.

e2e/133 pins the whole chain in the emulator, from both sides: the subset
builds, runs `ranges::sort` with a projection plus optional/atomic/span/
string_view on the target, and `std::mutex` fails to COMPILE — capabilities
turned off in `__config_site` vanish rather than leaving a run-time stub.

The `import std` diagnostic names `std-freestanding` again, now that the
package is published.
…e one

Both bare-metal packages carried `[xlings] deps = ["xim:picolibc-riscv@1.8.12"]`
and the standard-library subset also carried `xim:llvm`. That pinned a board
package and an implementation-neutral library alike to one libc, one ISA, one
toolchain and one version of each — none of which is a property of either.

The cause was a gap in the target model rather than sloppy packaging. A hosted
target has always had its C library resolved for it: musl rides inside its gcc
payload and glibc arrives through PayloadPaths, which is why nobody writes
`xim:glibc` in a manifest. A freestanding target pins a generic clang, which
carries no target libc, and there was no axis for one — so the requirement
leaked outward into every package.

kKnownTargets now names the target's C library beside its compiler pin. It is
installed through the same channel `[xlings] deps` already use, its headers go
on every compile line and its directory on the link search path. Location is a
target fact; selection stays a board fact — a board still chooses
`-lcrt0-semihost` over a UART crt0, and still names its linker script.

Two interfaces let a package ask instead of declare: `mcpp::toolchain_dir()`
for headers that ship with the toolchain (libc++'s, for the freestanding
subset) and `mcpp::sysroot_dir()` for a file inside the target's C library (a
linker script). Both follow whatever `[toolchain]` and `--target` resolved.

Three things this turned up:

  * `-L` appended to the ordinary ldflags is discarded — a freestanding link
    line is replaced wholesale, so it has to go through LinkInputs. Measured:
    the flag was built and then simply was not there.
  * `link-script` resolves a bare name against the PACKAGE root, so a board
    cannot rely on the linker's search path for it; it asks for the sysroot.
  * e2e/131's privacy assertion was testing the wrong thing. It proved a
    dependency's `include-dir` stays private by showing `#include <stdio.h>`
    fails — but with the libc owned by the target that now correctly SUCCEEDS,
    exactly as on a hosted build. Split into two: the target's C headers must
    reach the consumer, and a header the BOARD ships must not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants