// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; // C++ exceptions in a wasm build, run under wasmtime: the fixture throws // through frames whose destructors have to run, catches the exception, // catches one from a libc++ header (vector::at) and one thrown inside // libc++.a itself (stoi), and with an argument lets one escape main, which // has to end the run as an uncaught exception rather than exit 0, trap or // hang. Its C source uses setjmp, which compiles and links next to the C++ // objects only when C gets the same wasm flags, and links at all only // because the build adds libsetjmp: the configuration asks for nothing. // // wasm builds used to be compiled with -fno-exceptions, so a throw did not // compile and libc++'s own throws aborted. Each part of turning them on fails // in its own way, and this is the test that sees it: a std module or a unit // built without exceptions (clang rejects the BMI), a link without libunwind // (undefined __cpp_exception), the libc++ built without exceptions (stoi // aborts), or LLVM's default legacy encoding, which wasmtime refuses to load. // Skips like WasmHelloRuns when there is no exception-enabled WASI sysroot or // no wasmtime; inside the Flatpak both are bundled. namespace { std::int32_t failures = 0; void Check(bool cond, std::string_view msg) { if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++failures; } } } int main() { const Toolchain& tc = GetToolchain(); const fs::path libDir = tc.wasiSysroot / "lib" / "wasm32-wasip1"; if (!fs::exists(libDir / "eh" / "libunwind.a") && !fs::exists(libDir / "libunwind.a")) { std::println(std::cerr, "SKIP: no exception-enabled WASI sysroot at {}", tc.wasiSysroot.string()); return 77; } if (std::system("which wasmtime > /dev/null 2>&1") != 0) { std::println(std::cerr, "SKIP: wasmtime not on PATH"); return 77; } Configuration cfg; cfg.path = fs::current_path() / "tests" / "WasmExceptions" / "fixture"; cfg.name = "wasm-exceptions"; cfg.outputName = "wasm-exceptions"; cfg.target = "wasm32-wasip1"; cfg.type = ConfigurationType::Executable; std::array ifaces = {}; std::array impls = { "main" }; cfg.GetInterfacesAndImplementations(ifaces, impls); cfg.cFiles = { cfg.path / "jump" }; std::unordered_map> depResults; std::mutex depMutex; const BuildResult r = Build(cfg, depResults, depMutex); if (!r.result.empty()) { std::println(std::cerr, "build failed: {}", r.result); return 1; } const fs::path wasm = ExpectedOutput(cfg); const auto caught = RunCommandWithTimeout(std::format("wasmtime {}", wasm.string()), std::chrono::seconds(60)); Check(caught.exitCode == 0, std::format("the run that catches everything exits 0 (exit {}): {}", caught.exitCode, caught.output)); Check(caught.output.contains("caught: thrown in wasm after 4 destructors"), std::format("the exception is caught after unwinding all four frames: {}", caught.output)); Check(caught.output.contains("caught: out_of_range from libc++"), std::format("an exception from a libc++ header is catchable: {}", caught.output)); Check(caught.output.contains("caught: invalid_argument from inside libc++"), std::format("an exception from inside libc++.a is catchable: {}", caught.output)); Check(caught.output.contains("longjmp came back with 7"), std::format("setjmp in a C source works next to the C++ objects: {}", caught.output)); const auto escaped = RunCommandWithTimeout(std::format("wasmtime {} uncaught", wasm.string()), std::chrono::seconds(60)); Check(escaped.exitCode != 0 && !escaped.timedOut && !escaped.crashed && escaped.output.contains("thrown Wasm exception"), std::format("an exception escaping main ends the run as an uncaught exception (exit {}): {}", escaped.exitCode, escaped.output)); if (failures > 0) { std::println(std::cerr, "{} assertions failed", failures); return 1; } return 0; }