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/<triple>/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 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 05:54:39 +01:00
commit 63c4040000
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
4 changed files with 124 additions and 37 deletions

View file

@ -11,6 +11,34 @@ import :Toolchain;
import :Agents;
namespace fs = std::filesystem;
namespace Crafter {
// What every wasm32 C++ translation unit is compiled with, and the std
// module it imports. A BMI cannot be loaded by a unit whose options differ
// from the ones that wrote it, so GetCompileCommand and BuildStdPcm both
// take the set from here (through WasmCompileFlags), and VariantId and
// StdPcmDir hash it so that a crafter-build which changes it never reuses
// what the old set compiled.
//
// -fwasm-exceptions compiles C++ exceptions to WebAssembly exception
// handling. It also selects the sysroot's include/<triple>/eh and
// lib/<triple>/eh, the libc++ built with exceptions that wasi-sdk 33 and
// later ship next to the noeh/ one, so it has to reach the link too; the
// link reuses this command line, so it does. Those libraries use the
// standardized form of the proposal (try_table, exnref), while LLVM 23
// still defaults to the legacy one: wasmtime refuses to load that, and it
// would mix two encodings in one binary. -wasm-use-legacy-eh=false asks
// for the standard form, for setjmp as well. wasi-libc's setjmp.h does
// not compile without -wasm-enable-sjlj, which lowers setjmp to the same
// exception handling, and signal.h needs the emulation define.
inline constexpr std::string_view WasmBaselineFlags = "-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL";
// The wasi-runtime/runtime.js probes an engine has to pass to run anything
// built with WasmBaselineFlags; EnableWasiBrowserRuntime puts them on every
// entry of variants.json. Exception handling with exnref is the newest
// feature those flags need (Chrome 137, Firefox 131, Safari 18.4), so an
// engine that has it has SIMD128 and the rest as well.
inline constexpr auto WasmBaselineProbes = std::to_array<std::string_view>({"exception-handling"});
}
export namespace Crafter {
struct BuildResult {
std::string result;
@ -38,8 +66,10 @@ export namespace Crafter {
// `label` emits an additional `outputName.<label>.wasm` alongside the
// baseline `outputName.wasm`; `probes` names the runtime feature-detect
// checks (see wasi-runtime/runtime.js's probe registry) that must all pass
// for a browser to be served this variant. The first variant whose probes
// pass wins; the baseline is the universal fallback.
// for a browser to be served this variant, on top of WasmBaselineProbes,
// which every build needs. The first variant whose probes pass wins; the
// baseline is the fallback, and an engine that fails even its probes gets
// an error naming them.
struct WasmVariant {
std::string label;
std::vector<std::string> flags;
@ -586,6 +616,16 @@ export namespace Crafter {
compileKey += "|W:";
compileKey += f;
}
// So does the set every wasm unit gets before those. Unkeyed, the
// release that turned exceptions on would have kept the objects
// and PCMs compiled without them: the first unit recompiled after
// the update would import BMIs built without exceptions, which
// clang rejects, and objects nothing recompiled would let
// exceptions pass through without running their destructors.
if (target.starts_with("wasm32")) {
compileKey += "|B:";
compileKey += WasmBaselineFlags;
}
// Sorted by ApplyStandardArgs, so flag order on the command line
// doesn't split the cache.
for (const std::string& a : projectArgs) {
@ -622,7 +662,8 @@ export namespace Crafter {
// Directory holding the std module PCM for this Configuration. Keyed on
// target+march because a BMI is only loadable by a TU with matching target
// features, and suffixed with the wasm variant flags for the same reason.
// features, and suffixed with the wasm flags (WasmBaselineFlags, then the
// variant's) for the same reason.
fs::path StdPcmDir(const Configuration& config);
// Where Build leaves this Configuration's linked artifact: the executable
@ -739,3 +780,11 @@ export namespace Crafter {
// for-arg-in-args loop.
ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
}
namespace Crafter {
// WasmBaselineFlags followed by the active variant pass's
// wasmVariantFlags, each with a leading space: the wasm part of every
// compile command and of the std module's, which have to agree flag for
// flag.
std::string WasmCompileFlags(const Configuration& config);
}