From 63c4040000714d0f6164d1e123e01dd737a1b8d1 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 05:54:39 +0100 Subject: [PATCH] feat(wasm): compile C++ with WebAssembly exception handling wasm builds were compiled with -fno-exceptions. A throw did not compile, libc++'s own throws (vector::at, std::format, filesystem) aborted the program, and code that relies on exceptions could not be built for the browser at all. Every wasm unit and the std module now get -fwasm-exceptions, and the link adds -lunwind, which code built that way calls into and the driver does not add. The flag also selects the libc++ under the sysroot's lib//eh, which wasi-sdk 33 and later ship next to the noeh one. Those libraries use the standardized encoding (try_table, exnref) while LLVM 23 still emits the legacy one by default, which wasmtime refuses to load and which would put both in one binary, so the flags ask for the standard form with -wasm-use-legacy-eh=false. That makes the setjmp lowering loadable under wasmtime too. The flags live in one constant, WasmBaselineFlags, which the compile command and the std module both take through WasmCompileFlags: a BMI is refused by a unit whose exception setting differs from its own. VariantId and StdPcmDir hash it, so the first build after the update starts fresh instead of importing PCMs built without exceptions, and two releases sharing the Flatpak's cache don't take turns rebuilding one std PCM. A sysroot without the exception-enabled libraries stops the build with a line saying so, rather than at the link with "unable to find library -lunwind". The result needs an engine with exnref: Chrome 137, Firefox 131, Safari 18.4, the bundled wasmtime. The runtime's exception-handling probe tested the legacy encoding; it now tests try_table, every variants.json entry carries it, and a browser without it is told which feature it lacks instead of failing with a CompileError. There is no switch back to -fno-exceptions: engines without exnref are old enough not to keep a second build for them. Co-Authored-By: Claude Opus 5.5 --- implementations/Crafter.Build-Clang.cpp | 64 +++++++++++++++------- implementations/Crafter.Build-Platform.cpp | 26 +++++---- interfaces/Crafter.Build-Clang.cppm | 55 ++++++++++++++++++- wasi-runtime/runtime.js | 16 +++++- 4 files changed, 124 insertions(+), 37 deletions(-) diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 35b64d7..ba9f3d7 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" : ""; @@ -1139,6 +1152,12 @@ 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.