test(wasm): exceptions in a wasm build, and a sysroot without them
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:54:48 +01:00
// 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,
test(wasm): the cases the review found
WasmBuildRefusals checks that -flto in compileFlags and in linkFlags,
-fno-exceptions and a shared library are each refused with their
cause, and that none of them leaves bin/ or build/ behind. With the
check disabled, each of them failed on a missing std.cppm instead,
after the build directories had been made.
WasiSysrootWithoutExceptions also takes its sysroot through missing
and without the target's directory, each with a message of its own,
checks that a refused build leaves nothing behind, and that a static
library passes without libunwind.
WasmExceptions catches an exception thrown inside libc++.a (stoi).
vector::at throws from a header compiled into the fixture, so the
fixture also passed against the libc++ built without exceptions. Its C
source uses setjmp, which the old C flags did not compile. An
exception that escapes main has to end the run as wasmtime's uncaught
exception, where any nonzero exit used to pass, a trap or a timeout
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:10:41 +01:00
// 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.
test(wasm): exceptions in a wasm build, and a sysroot without them
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:54:48 +01:00
//
// 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
test(wasm): the cases the review found
WasmBuildRefusals checks that -flto in compileFlags and in linkFlags,
-fno-exceptions and a shared library are each refused with their
cause, and that none of them leaves bin/ or build/ behind. With the
check disabled, each of them failed on a missing std.cppm instead,
after the build directories had been made.
WasiSysrootWithoutExceptions also takes its sysroot through missing
and without the target's directory, each with a message of its own,
checks that a refused build leaves nothing behind, and that a static
library passes without libunwind.
WasmExceptions catches an exception thrown inside libc++.a (stoi).
vector::at throws from a header compiled into the fixture, so the
fixture also passed against the libc++ built without exceptions. Its C
source uses setjmp, which the old C flags did not compile. An
exception that escapes main has to end the run as wasmtime's uncaught
exception, where any nonzero exit used to pass, a trap or a timeout
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:10:41 +01:00
// (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.
test(wasm): exceptions in a wasm build, and a sysroot without them
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:54:48 +01:00
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 < fs : : path , 0 > ifaces = { } ;
std : : array < fs : : path , 1 > impls = { " main " } ;
cfg . GetInterfacesAndImplementations ( ifaces , impls ) ;
test(wasm): the cases the review found
WasmBuildRefusals checks that -flto in compileFlags and in linkFlags,
-fno-exceptions and a shared library are each refused with their
cause, and that none of them leaves bin/ or build/ behind. With the
check disabled, each of them failed on a missing std.cppm instead,
after the build directories had been made.
WasiSysrootWithoutExceptions also takes its sysroot through missing
and without the target's directory, each with a message of its own,
checks that a refused build leaves nothing behind, and that a static
library passes without libunwind.
WasmExceptions catches an exception thrown inside libc++.a (stoi).
vector::at throws from a header compiled into the fixture, so the
fixture also passed against the libc++ built without exceptions. Its C
source uses setjmp, which the old C flags did not compile. An
exception that escapes main has to end the run as wasmtime's uncaught
exception, where any nonzero exit used to pass, a trap or a timeout
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:10:41 +01:00
cfg . cFiles = { cfg . path / " jump " } ;
// setjmp and longjmp live in libsetjmp, which the build does not link
// on its own.
cfg . linkFlags . push_back ( " -lsetjmp " ) ;
test(wasm): exceptions in a wasm build, and a sysroot without them
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:54:48 +01:00
std : : unordered_map < fs : : path , std : : shared_future < BuildResult > > 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 ) ) ;
test(wasm): the cases the review found
WasmBuildRefusals checks that -flto in compileFlags and in linkFlags,
-fno-exceptions and a shared library are each refused with their
cause, and that none of them leaves bin/ or build/ behind. With the
check disabled, each of them failed on a missing std.cppm instead,
after the build directories had been made.
WasiSysrootWithoutExceptions also takes its sysroot through missing
and without the target's directory, each with a message of its own,
checks that a refused build leaves nothing behind, and that a static
library passes without libunwind.
WasmExceptions catches an exception thrown inside libc++.a (stoi).
vector::at throws from a header compiled into the fixture, so the
fixture also passed against the libc++ built without exceptions. Its C
source uses setjmp, which the old C flags did not compile. An
exception that escapes main has to end the run as wasmtime's uncaught
exception, where any nonzero exit used to pass, a trap or a timeout
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:10:41 +01:00
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 ) ) ;
test(wasm): exceptions in a wasm build, and a sysroot without them
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:54:48 +01:00
const auto escaped = RunCommandWithTimeout ( std : : format ( " wasmtime {} uncaught " , wasm . string ( ) ) , std : : chrono : : seconds ( 60 ) ) ;
test(wasm): the cases the review found
WasmBuildRefusals checks that -flto in compileFlags and in linkFlags,
-fno-exceptions and a shared library are each refused with their
cause, and that none of them leaves bin/ or build/ behind. With the
check disabled, each of them failed on a missing std.cppm instead,
after the build directories had been made.
WasiSysrootWithoutExceptions also takes its sysroot through missing
and without the target's directory, each with a message of its own,
checks that a refused build leaves nothing behind, and that a static
library passes without libunwind.
WasmExceptions catches an exception thrown inside libc++.a (stoi).
vector::at throws from a header compiled into the fixture, so the
fixture also passed against the libc++ built without exceptions. Its C
source uses setjmp, which the old C flags did not compile. An
exception that escapes main has to end the run as wasmtime's uncaught
exception, where any nonzero exit used to pass, a trap or a timeout
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:10:41 +01:00
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 ) ) ;
test(wasm): exceptions in a wasm build, and a sysroot without them
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:54:48 +01:00
if ( failures > 0 ) {
std : : println ( std : : cerr , " {} assertions failed " , failures ) ;
return 1 ;
}
return 0 ;
}