// 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 that libc++ throws itself (vector::at), and with an argument // lets one escape main, which has to fail the run rather than exit 0. // // 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), 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); 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("libc++'s own exception is catchable: {}", caught.output)); const auto escaped = RunCommandWithTimeout(std::format("wasmtime {} uncaught", wasm.string()), std::chrono::seconds(60)); Check(escaped.exitCode != 0, std::format("an exception escaping main fails the run: {}", escaped.output)); if (failures > 0) { std::println(std::cerr, "{} assertions failed", failures); return 1; } return 0; }