You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(freestanding): exceptions off graph-wide, and the std subset becomes usable (#459)
* feat(freestanding): exceptions off graph-wide, and the std subset becomes 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.
* feat(target): the target owns its C library, so no package has to name 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.
---------
Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Copy file name to clipboardExpand all lines: docs/05-mcpp-toml.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -931,7 +931,9 @@ that has none.
931
931
|---|---|
932
932
| Link line |`-nostdlib -nostartfiles -static`, and nothing hosted — no crt files, no dynamic linker, no C++ runtime. The linker is addressed by **absolute path** (`-fuse-ld=<payload>/bin/ld.lld`), because `-fuse-ld=lld` resolves through `PATH` and finds GNU ld on any machine with binutils earlier on it. |
933
933
| ISA flags |`-march` / `-mabi` / `-mcmodel` come from the target table, so `--target <triple>` alone is enough to produce a correct object file. |
934
-
|`import std`|**Unavailable.**`std` is one module over the entire library — threads, filesystem and iostreams included — so there is no subset of it to build without an OS. What a firmware imports instead is the module its **board package** exports, which is where the target's C library is already wrapped. |
934
+
| C library |**The target's**, resolved by mcpp from the target's own row exactly as the compiler is — a bare-metal project declares no libc, just as a hosted one declares no glibc. Its headers reach every translation unit and its directory is on the link search path, so a board package selects out of it by bare name (`-lc`, `-lcrt0-semihost`). *Which* objects and *which* linker script remain board decisions. |
935
+
| Exceptions and RTTI |**Off**, on every translation unit including a dependency's. There is no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would otherwise pull in `__cxa_throw` and three more undefined symbols. It belongs to the target rather than to a project's `cxxflags` because a BMI records it — a dependency compiled with exceptions cannot be imported by a unit without them. |
936
+
|`import std`|**Unavailable.**`std` is one module over the entire library — threads, filesystem and iostreams included — so there is no subset of it to build without an OS. Two ordinary dependencies replace it: the **board package** wraps the target's C library, and **`std-freestanding`** carries the parts of the standard library that need no OS (103 of libc++'s 110 headers, measured). |
935
937
| Entry point |`int main()` works **as long as something supplies a `crt0`** — a board package normally does, and then a firmware's entry point is an ordinary `main` whose return value reaches the host through semihosting. Only a zero-libc board needs an explicit target whose `main` points at the file carrying `_start`. |
0 commit comments