diff --git a/AGENTS.md b/AGENTS.md index d6be6d2..1c445f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,7 +117,7 @@ Beyond the rules: - There is no host-platform preprocessor split: the host is Linux. Target differences (mingw, wasm) branch on `cfg.target` strings at runtime, in the library as well as in `project.cpp` and tests. - A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`. - Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones. -- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, wasm variant flags, uninterpreted project args and the toolchain (`Toolchain::Key()`). +- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, the wasm baseline and variant flags, uninterpreted project args and the toolchain (`Toolchain::Key()`). ## Design rules @@ -191,5 +191,7 @@ Local builds are two scripts in `project.cpp`. Run them with the native binary: - The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop. - A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone. - A std module source only compiles against the headers of its own libc++ release, and a sysroot does not always ship one (Alpine). The Flatpak therefore carries the sources of the release next to the bundled one under `share/crafter-build/libc++-modules//`, one `libcxx--modules` manifest module each. When the bundled LLVM is bumped, keep the release it leaves behind as such a module: sysroots on the stable distro stay on it for months. The native loop has no such directory; stage one by hand under `share/crafter-build/` to test against a foreign sysroot. +- The wasm compile flags live in one place, `WasmBaselineFlags` in `Crafter.Build-Clang.cppm`: the std module and every unit have to agree on them, and `VariantId()` and the std PCM directory hash them, so a change rebuilds instead of mixing old and new BMIs. They enable C++ exceptions, so the link takes the exception-enabled libc++ and libunwind from the sysroot's `lib//eh/` (wasi-sdk 33 and later). The Flatpak's sysroot has them and a distro's may not; where they are missing the wasm tests skip, so run those through the Flatpak. A browser page checks the engine with the `WasmBaselineProbes` named in `variants.json`. +- `WasiBrowserRuntimeExit` runs `wasi-runtime/runtime.js` under node and skips where there is none. The Flatpak has no node, so CI never runs it: after touching `runtime.js`, run the suite in the native loop with node on `PATH`. - `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe---*/` rather than predicting the hash. The hash includes the compiler, so after a toolchain update the previous variant's directories stay in `bin/` and `build/` until `clean`, and such a glob matches both. - The self-host build and `test` keep the dependency notes section of this very file. None of Crafter.Math, Crafter.Asset and Crafter.LintRules ships an `AGENTS.consumer.md` yet. Once one does, a build here can change AGENTS.md whenever that repository changes its notes, since all three are unpinned. The tree is then dirty, so the next build's `git describe --dirty` version differs, which is a new `crafter.build-lib` variant and a full rebuild. Commit the section to settle it. diff --git a/README.md b/README.md index 53844fe..77514ec 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,16 @@ For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sy For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed, with libstdc++ 15 or newer for its std module. clang finds it by the `x86_64-w64-mingw32-gcc` on `PATH`; `CRAFTER_MINGW_DIR` names another prefix (the directory holding `x86_64-w64-mingw32/` and `lib/gcc/`), and inside the Flatpak the host's `/usr` is used when it has mingw-w64. Without one the build stops with a message saying where it looked. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported, there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags. +For `--target=wasm32-wasip1`, the WASI sysroot is the one beside the toolchain (the Flatpak bundles one) unless `CRAFTER_BUILD_WASI_SYSROOT` names another, and tests run under `wasmtime`. C++ exceptions work as on the other targets: every unit and the std module are compiled with `-fwasm-exceptions` in the standardized form of WebAssembly exception handling (exnref), against the exception-enabled libc++ in the sysroot's `lib//eh/`, and `-lunwind` is linked automatically. A sysroot without that libc++ (wasi-sdk before 33) stops the build with one line saying so. The result runs on engines with that proposal: Chrome 137, Firefox 131 and Safari 18.4 or newer, and the wasmtime the Flatpak bundles. A page from `EnableWasiBrowserRuntime` checks first and names what an older browser lacks in the console, instead of failing to compile the module. An exception that escapes `main` leaves the program as an uncaught WebAssembly exception (wasmtime reports `thrown Wasm exception` and exits 1, a page gets a `WebAssembly.Exception`) without calling `std::terminate`, so a `std::set_terminate` handler does not run and nothing prints the exception's type or `what()`: wasm exception handling unwinds in a single phase, and nothing finds out that no handler is left until the stack is gone. Catch it in `main` to report it. + +A wasm build refuses three things before it starts, each with a message that says why: `-flto` in `compileFlags` or `linkFlags` (LTO code generation drops the exception handling, [WebAssembly/wasi-sdk#629](https://github.com/WebAssembly/wasi-sdk/issues/629)), `-fno-exceptions` (the std module is built with exceptions, and a unit without them cannot import it) and a shared library (the exception-enabled libc++ is static archives only). `setjmp` and `longjmp` work in C and C++ sources, with `-lsetjmp` linked automatically like `-lunwind`; a `longjmp` across a `noexcept` function ends the program, because it is a wasm exception and the function's terminate handler catches it. Outside the Flatpak, a wasmtime older than 47 has exception handling off and needs `-W exceptions=y`, and a `wasm-opt` on `PATH` from a binaryen that cannot read the standardized encoding breaks release links, since clang runs it on optimized wasm links when it finds one. + +In a page from `EnableWasiBrowserRuntime`, `exit` and `_Exit` end the program as they would anywhere else: none of its code runs on the way out, no destructor and no `catch (...)`, its status is in `window.crafter_wasi.exitCode`, and bridges that call into it on their own are expected to stop. A program that hands the thread to the browser for good and goes on serving calls, as Crafter.Graphics's `Window::StartSync()` does, parks instead of exiting. Parking never returns, runs nothing of the program either, and leaves `main`'s locals valid for the calls that follow: + +```cpp +extern "C" [[noreturn]] __attribute__((import_module("crafter_runtime"), import_name("park"))) void Park(); +``` + ### Configuring runners Two layers, CLI wins over env, both override the per-test default of `Local()`: diff --git a/examples/wasi/README.md b/examples/wasi/README.md index 7ac0f42..8a8db56 100644 --- a/examples/wasi/README.md +++ b/examples/wasi/README.md @@ -4,9 +4,14 @@ Build a C++26 program for the `wasm32-wasi` target and run it in a browser. ## Prerequisites -- `wasi-libc`, `wasi-libc++`, `wasi-libc++abi` (Arch packages — provide - `/usr/share/wasi-sysroot/`, which `crafter-build` finds automatically). - Other distros: install the WASI SDK and set `cfg.sysroot` in `project.cpp`. +- A WASI sysroot with the exception-enabled libc++, `lib/wasm32-wasip1/eh/` + from wasi-sdk 33 or later: wasm builds compile C++ exceptions and link those + libraries. The Flatpak bundles one, which `crafter-build` uses as it is. + Elsewhere, point `CRAFTER_BUILD_WASI_SYSROOT` at a wasi-sdk sysroot, or set + `cfg.sysroot` in `project.cpp`. Arch's `wasi-libc++` packages have no such + libraries, and the build stops with a message saying so. +- For the browser: WebAssembly exception handling, in Chrome 137, Firefox 131, + Safari 18.4 or newer. ## Build diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 35b64d7..e0d8329 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -256,6 +256,15 @@ fs::path Crafter::StdPcmDir(const Configuration& config) { // the consuming TUs see a target-feature mismatch. Suffix the cache dir // with the variant flags when present. std::string stdPcmKey = std::format("{}-{}-{}", config.target, config.march, GetToolchain().Key()); + // The flags every wasm unit gets are compiled into the std PCM as well, + // and the importing units have to match them. Two releases that share a + // cache and a compiler, as the Flatpak's branches do, would otherwise + // take turns rebuilding this one std PCM, each time underneath the PCMs + // built against the previous one, and clang crashes on those rather than + // rejecting them. + if (config.target.starts_with("wasm32")) { + stdPcmKey += std::format("-{:08x}", static_cast(std::hash{}(WasmBaselineFlags))); + } for (const std::string& f : config.wasmVariantFlags) { stdPcmKey += "+"; for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c; @@ -263,6 +272,12 @@ fs::path Crafter::StdPcmDir(const Configuration& config) { return GetCacheDir()/stdPcmKey; } +std::string Crafter::WasmCompileFlags(const Configuration& config) { + std::string flags = std::format(" {}", WasmBaselineFlags); + for (const std::string& f : config.wasmVariantFlags) flags += std::format(" {}", f); + return flags; +} + fs::path Crafter::ExpectedOutput(const Configuration& config) { const fs::path dir = config.BinDir(); const bool mingwTarget = config.target == "x86_64-w64-mingw32"; @@ -317,15 +332,11 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) { if (isWasm) { // -mllvm is consumed by codegen but not the link driver, which is the // same command line; quiet the unused-flag warning rather than split - // compile and link commands. - out.command += " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument"; - // Active variant pass (see the variant driver at the end of Build): - // extra codegen flags (e.g. -mrelaxed-simd) applied to every TU. Empty - // for the baseline build. Part of VariantId, so these objects/PCMs - // land in their own dir. - for (const std::string& f : config.wasmVariantFlags) { - out.command += std::format(" {}", f); - } + // compile and link commands. An active variant pass (see the variant + // driver at the end of Build) adds its codegen flags (e.g. + // -mrelaxed-simd) to every TU; they are part of VariantId, so those + // objects/PCMs land in their own dir. + out.command += std::format("{} -Wno-unused-command-line-argument", WasmCompileFlags(config)); } if (config.target == "x86_64-w64-mingw32") { // mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without @@ -405,12 +416,14 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) { // (cross-TU inlining) plus dead-section GC and safe identical-code folding. // Default-on in Release, never Debug (keeps builds fast and debuggable). // Excluded for wasm32 — its -mllvm/sjlj codegen flags don't compose with - // LTO here — and for nvcc .cu objects below (they can't emit LLVM bitcode, - // so they link in as ordinary objects alongside the bitcode ones). ThinLTO - // rather than monolithic -flto so link time and memory scale with arbitrary - // user project sizes. Compile-side flags (-flto, -ffunction/-fdata-sections) - // ride on `command`, which is reused as the link base; the link-only -Wl - // flags go on linkExtras so they don't warn on each -c compile. + // LTO here, and wasi-sdk lists exceptions under LTO as broken + // (WebAssembly/wasi-sdk#629) — and for nvcc .cu objects below (they can't + // emit LLVM bitcode, so they link in as ordinary objects alongside the + // bitcode ones). ThinLTO rather than monolithic -flto so link time and + // memory scale with arbitrary user project sizes. Compile-side flags + // (-flto, -ffunction/-fdata-sections) ride on `command`, which is reused + // as the link base; the link-only -Wl flags go on linkExtras so they + // don't warn on each -c compile. out.useLto = !config.debug && !isWasm; out.ltoCompileFlags = out.useLto ? " -flto=thin -ffunction-sections -fdata-sections" : ""; out.ltoLinkFlags = out.useLto ? " -flto=thin -Wl,--gc-sections -Wl,--icf=safe" : ""; @@ -480,6 +493,54 @@ namespace { walk(config, config.name); return error; } + + // What a wasm32 configuration cannot be built with now that every unit + // has C++ exceptions, and what its sysroot has to have for that; empty + // for any other target and when nothing is wrong. Each would otherwise + // fail at the first compile or link with a message that does not say + // why, or not fail at all. + std::string WasmBuildError(const Configuration& config) { + if (!config.target.starts_with("wasm32")) return ""; + for (const std::vector* flags : { &config.compileFlags, &config.linkFlags }) { + for (const std::string& flag : *flags) { + // The compile has the backend options that keep exception + // handling; LTO code generation at the link does not, so it + // drops every landing pad and the build succeeds with throws + // that escape their catch blocks (WebAssembly/wasi-sdk#629). + if (flag.starts_with("-flto")) { + return std::format("{}: {} is not supported on wasm32: LTO code generation drops C++ exception handling (WebAssembly/wasi-sdk#629), so every throw would escape its catch", config.name, flag); + } + // The std module is built with exceptions, and a unit built + // without them cannot import it. + if (flag == "-fno-exceptions" || flag == "-fno-cxx-exceptions") { + return std::format("{}: {} is not supported on wasm32: wasm builds have C++ exceptions, the std module included, and a unit compiled without them cannot import std", config.name, flag); + } + } + } + // wasi-sdk's exception support does not cover shared libraries: the + // libc++ under lib//eh is static archives built without -fPIC. + if (config.type == ConfigurationType::LibraryDynamic) { + return std::format("{}: wasm32 shared libraries are not supported: the exception-enabled libc++ they would link has static archives only", config.name); + } + const fs::path sysroot = config.sysroot; + if (!fs::is_directory(sysroot)) { + return std::format("no WASI sysroot at {}: the Flatpak bundles one, and CRAFTER_BUILD_WASI_SYSROOT selects another", sysroot.string()); + } + const fs::path libDir = sysroot / "lib" / NormalizeTarget(config.target); + if (!fs::is_directory(libDir)) { + return std::format("the WASI sysroot at {} has nothing for {} (no {})", sysroot.string(), config.target, libDir.string()); + } + // Only a link takes the exception-enabled libraries: a static + // library's objects compile against the headers alone. The driver + // looks in eh/ first and then in the triple's own directory, where a + // sysroot built with exceptions only (WASI_SDK_EXCEPTIONS=ON) has them. + // Without them the first link fails with "unable to find library + // -lunwind", which says nothing about why it was asked for. + if (config.type == ConfigurationType::Executable && !fs::exists(libDir / "eh" / "libunwind.a") && !fs::exists(libDir / "libunwind.a")) { + return std::format("the WASI sysroot at {} has no C++ exception support (no {}): wasm builds use WebAssembly exception handling and need the exception-enabled libc++ and libunwind that wasi-sdk 33 and later ship. The Flatpak bundles such a sysroot; CRAFTER_BUILD_WASI_SYSROOT selects another", sysroot.string(), (libDir / "eh" / "libunwind.a").string()); + } + return ""; + } } BuildResult Crafter::Build(Configuration& config, std::unordered_map>& depResults, std::mutex& depMutex) { @@ -540,6 +601,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map.wasm (produced by Build's - // variant driver); the baseline maps to outputName.wasm. Even with no - // declared variants we still emit a one-entry manifest so the runtime has a + // as the fallback. Every entry carries WasmBaselineProbes besides its own: + // every file is built with the baseline flags, so an engine without + // exception handling can run none of them, and runtime.js tells it so + // instead of failing to compile the module. A variant with a non-empty + // label maps to outputName.