Compare commits
21 changed files with 81 additions and 923 deletions
|
|
@ -117,7 +117,7 @@ Beyond the rules:
|
||||||
- There is no host-platform preprocessor split: the host is Linux. Target differences (mingw, wasm) branch on `cfg.target` strings at runtime, in the library as well as in `project.cpp` and tests.
|
- There is no host-platform preprocessor split: the host is Linux. Target differences (mingw, wasm) branch on `cfg.target` strings at runtime, in the library as well as in `project.cpp` and tests.
|
||||||
- A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`.
|
- A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`.
|
||||||
- Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones.
|
- Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones.
|
||||||
- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, the wasm baseline and variant flags, uninterpreted project args and the toolchain (`Toolchain::Key()`).
|
- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, wasm variant flags, uninterpreted project args and the toolchain (`Toolchain::Key()`).
|
||||||
|
|
||||||
## Design rules
|
## Design rules
|
||||||
|
|
||||||
|
|
@ -191,7 +191,5 @@ Local builds are two scripts in `project.cpp`. Run them with the native binary:
|
||||||
- The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop.
|
- The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop.
|
||||||
- A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone.
|
- A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone.
|
||||||
- A std module source only compiles against the headers of its own libc++ release, and a sysroot does not always ship one (Alpine). The Flatpak therefore carries the sources of the release next to the bundled one under `share/crafter-build/libc++-modules/<major>/`, one `libcxx-<major>-modules` manifest module each. When the bundled LLVM is bumped, keep the release it leaves behind as such a module: sysroots on the stable distro stay on it for months. The native loop has no such directory; stage one by hand under `share/crafter-build/` to test against a foreign sysroot.
|
- A std module source only compiles against the headers of its own libc++ release, and a sysroot does not always ship one (Alpine). The Flatpak therefore carries the sources of the release next to the bundled one under `share/crafter-build/libc++-modules/<major>/`, one `libcxx-<major>-modules` manifest module each. When the bundled LLVM is bumped, keep the release it leaves behind as such a module: sysroots on the stable distro stay on it for months. The native loop has no such directory; stage one by hand under `share/crafter-build/` to test against a foreign sysroot.
|
||||||
- The wasm compile flags live in one place, `WasmBaselineFlags` in `Crafter.Build-Clang.cppm`: the std module and every unit have to agree on them, and `VariantId()` and the std PCM directory hash them, so a change rebuilds instead of mixing old and new BMIs. They enable C++ exceptions, so the link takes the exception-enabled libc++ and libunwind from the sysroot's `lib/<triple>/eh/` (wasi-sdk 33 and later). The Flatpak's sysroot has them and a distro's may not; where they are missing the wasm tests skip, so run those through the Flatpak. A browser page checks the engine with the `WasmBaselineProbes` named in `variants.json`.
|
|
||||||
- `WasiBrowserRuntimeExit` runs `wasi-runtime/runtime.js` under node and skips where there is none. The Flatpak has no node, so CI never runs it: after touching `runtime.js`, run the suite in the native loop with node on `PATH`.
|
|
||||||
- `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe-<target>-<march>-*/` rather than predicting the hash. The hash includes the compiler, so after a toolchain update the previous variant's directories stay in `bin/` and `build/` until `clean`, and such a glob matches both.
|
- `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe-<target>-<march>-*/` rather than predicting the hash. The hash includes the compiler, so after a toolchain update the previous variant's directories stay in `bin/` and `build/` until `clean`, and such a glob matches both.
|
||||||
- The self-host build and `test` keep the dependency notes section of this very file. None of Crafter.Math, Crafter.Asset and Crafter.LintRules ships an `AGENTS.consumer.md` yet. Once one does, a build here can change AGENTS.md whenever that repository changes its notes, since all three are unpinned. The tree is then dirty, so the next build's `git describe --dirty` version differs, which is a new `crafter.build-lib` variant and a full rebuild. Commit the section to settle it.
|
- The self-host build and `test` keep the dependency notes section of this very file. None of Crafter.Math, Crafter.Asset and Crafter.LintRules ships an `AGENTS.consumer.md` yet. Once one does, a build here can change AGENTS.md whenever that repository changes its notes, since all three are unpinned. The tree is then dirty, so the next build's `git describe --dirty` version differs, which is a new `crafter.build-lib` variant and a full rebuild. Commit the section to settle it.
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -187,16 +187,6 @@ For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sy
|
||||||
|
|
||||||
For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed, with libstdc++ 15 or newer for its std module. clang finds it by the `x86_64-w64-mingw32-gcc` on `PATH`; `CRAFTER_MINGW_DIR` names another prefix (the directory holding `x86_64-w64-mingw32/` and `lib/gcc/`), and inside the Flatpak the host's `/usr` is used when it has mingw-w64. Without one the build stops with a message saying where it looked. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported, there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags.
|
For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed, with libstdc++ 15 or newer for its std module. clang finds it by the `x86_64-w64-mingw32-gcc` on `PATH`; `CRAFTER_MINGW_DIR` names another prefix (the directory holding `x86_64-w64-mingw32/` and `lib/gcc/`), and inside the Flatpak the host's `/usr` is used when it has mingw-w64. Without one the build stops with a message saying where it looked. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported, there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags.
|
||||||
|
|
||||||
For `--target=wasm32-wasip1`, the WASI sysroot is the one beside the toolchain (the Flatpak bundles one) unless `CRAFTER_BUILD_WASI_SYSROOT` names another, and tests run under `wasmtime`. C++ exceptions work as on the other targets: every unit and the std module are compiled with `-fwasm-exceptions` in the standardized form of WebAssembly exception handling (exnref), against the exception-enabled libc++ in the sysroot's `lib/<triple>/eh/`, and `-lunwind` is linked automatically. A sysroot without that libc++ (wasi-sdk before 33) stops the build with one line saying so. The result runs on engines with that proposal: Chrome 137, Firefox 131 and Safari 18.4 or newer, and the wasmtime the Flatpak bundles. A page from `EnableWasiBrowserRuntime` checks first and names what an older browser lacks in the console, instead of failing to compile the module. An exception that escapes `main` leaves the program as an uncaught WebAssembly exception (wasmtime reports `thrown Wasm exception` and exits 1, a page gets a `WebAssembly.Exception`) without calling `std::terminate`, so a `std::set_terminate` handler does not run and nothing prints the exception's type or `what()`: wasm exception handling unwinds in a single phase, and nothing finds out that no handler is left until the stack is gone. Catch it in `main` to report it.
|
|
||||||
|
|
||||||
A wasm build refuses three things before it starts, each with a message that says why: `-flto` in `compileFlags` or `linkFlags` (LTO code generation drops the exception handling, [WebAssembly/wasi-sdk#629](https://github.com/WebAssembly/wasi-sdk/issues/629)), `-fno-exceptions` (the std module is built with exceptions, and a unit without them cannot import it) and a shared library (the exception-enabled libc++ is static archives only). `setjmp` and `longjmp` work in C and C++ sources, with `-lsetjmp` linked automatically like `-lunwind`; a `longjmp` across a `noexcept` function ends the program, because it is a wasm exception and the function's terminate handler catches it. Outside the Flatpak, a wasmtime older than 47 has exception handling off and needs `-W exceptions=y`, and a `wasm-opt` on `PATH` from a binaryen that cannot read the standardized encoding breaks release links, since clang runs it on optimized wasm links when it finds one.
|
|
||||||
|
|
||||||
In a page from `EnableWasiBrowserRuntime`, `exit` and `_Exit` end the program as they would anywhere else: none of its code runs on the way out, no destructor and no `catch (...)`, its status is in `window.crafter_wasi.exitCode`, and bridges that call into it on their own are expected to stop. A program that hands the thread to the browser for good and goes on serving calls, as Crafter.Graphics's `Window::StartSync()` does, parks instead of exiting. Parking never returns, runs nothing of the program either, and leaves `main`'s locals valid for the calls that follow:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
extern "C" [[noreturn]] __attribute__((import_module("crafter_runtime"), import_name("park"))) void Park();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuring runners
|
### Configuring runners
|
||||||
|
|
||||||
Two layers, CLI wins over env, both override the per-test default of `Local()`:
|
Two layers, CLI wins over env, both override the per-test default of `Local()`:
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,9 @@ Build a C++26 program for the `wasm32-wasi` target and run it in a browser.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- A WASI sysroot with the exception-enabled libc++, `lib/wasm32-wasip1/eh/`
|
- `wasi-libc`, `wasi-libc++`, `wasi-libc++abi` (Arch packages — provide
|
||||||
from wasi-sdk 33 or later: wasm builds compile C++ exceptions and link those
|
`/usr/share/wasi-sysroot/`, which `crafter-build` finds automatically).
|
||||||
libraries. The Flatpak bundles one, which `crafter-build` uses as it is.
|
Other distros: install the WASI SDK and set `cfg.sysroot` in `project.cpp`.
|
||||||
Elsewhere, point `CRAFTER_BUILD_WASI_SYSROOT` at a wasi-sdk sysroot, or set
|
|
||||||
`cfg.sysroot` in `project.cpp`. Arch's `wasi-libc++` packages have no such
|
|
||||||
libraries, and the build stops with a message saying so.
|
|
||||||
- For the browser: WebAssembly exception handling, in Chrome 137, Firefox 131,
|
|
||||||
Safari 18.4 or newer.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,15 +256,6 @@ fs::path Crafter::StdPcmDir(const Configuration& config) {
|
||||||
// the consuming TUs see a target-feature mismatch. Suffix the cache dir
|
// the consuming TUs see a target-feature mismatch. Suffix the cache dir
|
||||||
// with the variant flags when present.
|
// with the variant flags when present.
|
||||||
std::string stdPcmKey = std::format("{}-{}-{}", config.target, config.march, GetToolchain().Key());
|
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::uint32_t>(std::hash<std::string_view>{}(WasmBaselineFlags)));
|
|
||||||
}
|
|
||||||
for (const std::string& f : config.wasmVariantFlags) {
|
for (const std::string& f : config.wasmVariantFlags) {
|
||||||
stdPcmKey += "+";
|
stdPcmKey += "+";
|
||||||
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
|
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
|
||||||
|
|
@ -272,12 +263,6 @@ fs::path Crafter::StdPcmDir(const Configuration& config) {
|
||||||
return GetCacheDir()/stdPcmKey;
|
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) {
|
fs::path Crafter::ExpectedOutput(const Configuration& config) {
|
||||||
const fs::path dir = config.BinDir();
|
const fs::path dir = config.BinDir();
|
||||||
const bool mingwTarget = config.target == "x86_64-w64-mingw32";
|
const bool mingwTarget = config.target == "x86_64-w64-mingw32";
|
||||||
|
|
@ -332,11 +317,15 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
if (isWasm) {
|
if (isWasm) {
|
||||||
// -mllvm is consumed by codegen but not the link driver, which is the
|
// -mllvm is consumed by codegen but not the link driver, which is the
|
||||||
// same command line; quiet the unused-flag warning rather than split
|
// same command line; quiet the unused-flag warning rather than split
|
||||||
// compile and link commands. An active variant pass (see the variant
|
// compile and link commands.
|
||||||
// driver at the end of Build) adds its codegen flags (e.g.
|
out.command += " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument";
|
||||||
// -mrelaxed-simd) to every TU; they are part of VariantId, so those
|
// Active variant pass (see the variant driver at the end of Build):
|
||||||
// objects/PCMs land in their own dir.
|
// extra codegen flags (e.g. -mrelaxed-simd) applied to every TU. Empty
|
||||||
out.command += std::format("{} -Wno-unused-command-line-argument", WasmCompileFlags(config));
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (config.target == "x86_64-w64-mingw32") {
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
|
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
|
||||||
|
|
@ -416,14 +405,12 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
// (cross-TU inlining) plus dead-section GC and safe identical-code folding.
|
// (cross-TU inlining) plus dead-section GC and safe identical-code folding.
|
||||||
// Default-on in Release, never Debug (keeps builds fast and debuggable).
|
// Default-on in Release, never Debug (keeps builds fast and debuggable).
|
||||||
// Excluded for wasm32 — its -mllvm/sjlj codegen flags don't compose with
|
// Excluded for wasm32 — its -mllvm/sjlj codegen flags don't compose with
|
||||||
// LTO here, and wasi-sdk lists exceptions under LTO as broken
|
// LTO here — and for nvcc .cu objects below (they can't emit LLVM bitcode,
|
||||||
// (WebAssembly/wasi-sdk#629) — and for nvcc .cu objects below (they can't
|
// so they link in as ordinary objects alongside the bitcode ones). ThinLTO
|
||||||
// emit LLVM bitcode, so they link in as ordinary objects alongside the
|
// rather than monolithic -flto so link time and memory scale with arbitrary
|
||||||
// bitcode ones). ThinLTO rather than monolithic -flto so link time and
|
// user project sizes. Compile-side flags (-flto, -ffunction/-fdata-sections)
|
||||||
// memory scale with arbitrary user project sizes. Compile-side flags
|
// ride on `command`, which is reused as the link base; the link-only -Wl
|
||||||
// (-flto, -ffunction/-fdata-sections) ride on `command`, which is reused
|
// flags go on linkExtras so they don't warn on each -c compile.
|
||||||
// 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.useLto = !config.debug && !isWasm;
|
||||||
out.ltoCompileFlags = out.useLto ? " -flto=thin -ffunction-sections -fdata-sections" : "";
|
out.ltoCompileFlags = out.useLto ? " -flto=thin -ffunction-sections -fdata-sections" : "";
|
||||||
out.ltoLinkFlags = out.useLto ? " -flto=thin -Wl,--gc-sections -Wl,--icf=safe" : "";
|
out.ltoLinkFlags = out.useLto ? " -flto=thin -Wl,--gc-sections -Wl,--icf=safe" : "";
|
||||||
|
|
@ -493,54 +480,6 @@ namespace {
|
||||||
walk(config, config.name);
|
walk(config, config.name);
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// What a wasm32 configuration cannot be built with now that every unit
|
|
||||||
// has C++ exceptions, and what its sysroot has to have for that; empty
|
|
||||||
// for any other target and when nothing is wrong. Each would otherwise
|
|
||||||
// fail at the first compile or link with a message that does not say
|
|
||||||
// why, or not fail at all.
|
|
||||||
std::string WasmBuildError(const Configuration& config) {
|
|
||||||
if (!config.target.starts_with("wasm32")) return "";
|
|
||||||
for (const std::vector<std::string>* flags : { &config.compileFlags, &config.linkFlags }) {
|
|
||||||
for (const std::string& flag : *flags) {
|
|
||||||
// The compile has the backend options that keep exception
|
|
||||||
// handling; LTO code generation at the link does not, so it
|
|
||||||
// drops every landing pad and the build succeeds with throws
|
|
||||||
// that escape their catch blocks (WebAssembly/wasi-sdk#629).
|
|
||||||
if (flag.starts_with("-flto")) {
|
|
||||||
return std::format("{}: {} is not supported on wasm32: LTO code generation drops C++ exception handling (WebAssembly/wasi-sdk#629), so every throw would escape its catch", config.name, flag);
|
|
||||||
}
|
|
||||||
// The std module is built with exceptions, and a unit built
|
|
||||||
// without them cannot import it.
|
|
||||||
if (flag == "-fno-exceptions" || flag == "-fno-cxx-exceptions") {
|
|
||||||
return std::format("{}: {} is not supported on wasm32: wasm builds have C++ exceptions, the std module included, and a unit compiled without them cannot import std", config.name, flag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// wasi-sdk's exception support does not cover shared libraries: the
|
|
||||||
// libc++ under lib/<triple>/eh is static archives built without -fPIC.
|
|
||||||
if (config.type == ConfigurationType::LibraryDynamic) {
|
|
||||||
return std::format("{}: wasm32 shared libraries are not supported: the exception-enabled libc++ they would link has static archives only", config.name);
|
|
||||||
}
|
|
||||||
const fs::path sysroot = config.sysroot;
|
|
||||||
if (!fs::is_directory(sysroot)) {
|
|
||||||
return std::format("no WASI sysroot at {}: the Flatpak bundles one, and CRAFTER_BUILD_WASI_SYSROOT selects another", sysroot.string());
|
|
||||||
}
|
|
||||||
const fs::path libDir = sysroot / "lib" / NormalizeTarget(config.target);
|
|
||||||
if (!fs::is_directory(libDir)) {
|
|
||||||
return std::format("the WASI sysroot at {} has nothing for {} (no {})", sysroot.string(), config.target, libDir.string());
|
|
||||||
}
|
|
||||||
// Only a link takes the exception-enabled libraries: a static
|
|
||||||
// library's objects compile against the headers alone. The driver
|
|
||||||
// looks in eh/ first and then in the triple's own directory, where a
|
|
||||||
// sysroot built with exceptions only (WASI_SDK_EXCEPTIONS=ON) has them.
|
|
||||||
// Without them the first link fails with "unable to find library
|
|
||||||
// -lunwind", which says nothing about why it was asked for.
|
|
||||||
if (config.type == ConfigurationType::Executable && !fs::exists(libDir / "eh" / "libunwind.a") && !fs::exists(libDir / "libunwind.a")) {
|
|
||||||
return std::format("the WASI sysroot at {} has no C++ exception support (no {}): wasm builds use WebAssembly exception handling and need the exception-enabled libc++ and libunwind that wasi-sdk 33 and later ship. The Flatpak bundles such a sysroot; CRAFTER_BUILD_WASI_SYSROOT selects another", sysroot.string(), (libDir / "eh" / "libunwind.a").string());
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
|
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
|
||||||
|
|
@ -601,11 +540,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
||||||
config.sysroot = GetToolchain().wasiSysroot.string();
|
config.sysroot = GetToolchain().wasiSysroot.string();
|
||||||
}
|
}
|
||||||
// As early as the duplicate-module check, so a refused build leaves no
|
|
||||||
// directory behind and starts no thread, but after the sysroot is known.
|
|
||||||
if (std::string refused = WasmBuildError(config); !refused.empty()) {
|
|
||||||
return {std::move(refused), false, {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path buildDir = config.BuildDir();
|
fs::path buildDir = config.BuildDir();
|
||||||
fs::path outputDir = config.BinDir();
|
fs::path outputDir = config.BinDir();
|
||||||
|
|
@ -923,7 +857,8 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
// Same target-aware setup as the C++ compile path (line 459-): wasm32
|
// Same target-aware setup as the C++ compile path (line 459-): wasm32
|
||||||
// rejects -march, silently ignores -mtune, and needs --sysroot to find
|
// rejects -march, silently ignores -mtune, and needs --sysroot to find
|
||||||
// wasi-libc headers. Build the prefix once so all C compiles share it.
|
// wasi-libc headers. Build the prefix once so all C compiles share it.
|
||||||
std::string cArchFlags = isWasm
|
const bool cIsWasm = config.target.starts_with("wasm32");
|
||||||
|
std::string cArchFlags = cIsWasm
|
||||||
? std::string()
|
? std::string()
|
||||||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||||
if (!config.sysroot.empty()) {
|
if (!config.sysroot.empty()) {
|
||||||
|
|
@ -932,13 +867,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
// The C++ side gets it through GetBaseCommand.
|
// The C++ side gets it through GetBaseCommand.
|
||||||
cArchFlags += std::format(" --sysroot={}", GetToolchain().mingwSysroot.string());
|
cArchFlags += std::format(" --sysroot={}", GetToolchain().mingwSysroot.string());
|
||||||
}
|
}
|
||||||
if (isWasm) {
|
if (cIsWasm) {
|
||||||
// The C++ path's wasm flags, all of them, a variant pass's included.
|
// Matches the C++ path's wasi flag set so any libc shim defines
|
||||||
// With only the signal.h define, wasi-libc's setjmp.h refused to
|
// (e.g. _WASI_EMULATED_SIGNAL → signal.h shims) are visible to
|
||||||
// compile in C, and a C object given -wasm-enable-sjlj through
|
// C dependencies that drag in signal.h transitively.
|
||||||
// compileFlags used LLVM's legacy exception encoding next to C++
|
cArchFlags += " -D_WASI_EMULATED_SIGNAL";
|
||||||
// objects in the standard one, a mix engines refuse to load.
|
|
||||||
cArchFlags += WasmCompileFlags(config);
|
|
||||||
}
|
}
|
||||||
for (const fs::path& cFile : config.cFiles) {
|
for (const fs::path& cFile : config.cFiles) {
|
||||||
files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string());
|
files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string());
|
||||||
|
|
@ -1206,17 +1139,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
// compile so __once_callable et al resolve in static libstdc++.
|
// compile so __once_callable et al resolve in static libstdc++.
|
||||||
linkExtras += " -lstdc++exp -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread -Wl,-Bdynamic";
|
linkExtras += " -lstdc++exp -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread -Wl,-Bdynamic";
|
||||||
}
|
}
|
||||||
// Code built with -fwasm-exceptions calls into libunwind (the
|
|
||||||
// __cpp_exception tag, the landing-pad context, _Unwind_RaiseException),
|
|
||||||
// and -wasm-enable-sjlj turns setjmp and longjmp into calls into
|
|
||||||
// wasi-libc's libsetjmp (__wasm_setjmp, __wasm_longjmp, the __c_longjmp
|
|
||||||
// tag). The driver adds neither to the link the way it adds libc++. A
|
|
||||||
// static archive contributes only what something references, so a
|
|
||||||
// program without setjmp gets nothing from libsetjmp, and every sysroot
|
|
||||||
// that passes WasmBuildError ships both.
|
|
||||||
if (isWasm) {
|
|
||||||
linkExtras += " -lunwind -lsetjmp";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force a relink if the expected output is missing or older than any dep
|
// Force a relink if the expected output is missing or older than any dep
|
||||||
// artifact. Missing covers: previous build produced a different outputName,
|
// artifact. Missing covers: previous build produced a different outputName,
|
||||||
|
|
@ -1560,13 +1482,10 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
||||||
// Variant manifest. runtime.js fetches this, runs each entry's probes in
|
// Variant manifest. runtime.js fetches this, runs each entry's probes in
|
||||||
// order, and instantiates the first whose probes all pass — so the
|
// order, and instantiates the first whose probes all pass — so the
|
||||||
// preferred (most feature-rich) variants come first and the baseline last
|
// preferred (most feature-rich) variants come first and the baseline last
|
||||||
// as the fallback. Every entry carries WasmBaselineProbes besides its own:
|
// as the universal fallback (empty probes always pass). A variant with a
|
||||||
// every file is built with the baseline flags, so an engine without
|
// non-empty label maps to outputName.<label>.wasm (produced by Build's
|
||||||
// exception handling can run none of them, and runtime.js tells it so
|
// variant driver); the baseline maps to outputName.wasm. Even with no
|
||||||
// instead of failing to compile the module. A variant with a non-empty
|
// declared variants we still emit a one-entry manifest so the runtime has a
|
||||||
// label maps to outputName.<label>.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
|
|
||||||
// single, uniform code path.
|
// single, uniform code path.
|
||||||
auto jsonEscape = [](std::string_view s) {
|
auto jsonEscape = [](std::string_view s) {
|
||||||
std::string out;
|
std::string out;
|
||||||
|
|
@ -1581,14 +1500,12 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
||||||
std::ofstream v(variantsPath);
|
std::ofstream v(variantsPath);
|
||||||
v << "[";
|
v << "[";
|
||||||
bool first = true;
|
bool first = true;
|
||||||
auto emit = [&](const std::string& label, const std::vector<std::string>& ownProbes) {
|
auto emit = [&](const std::string& label, const std::vector<std::string>& probes) {
|
||||||
if (!first) v << ",";
|
if (!first) v << ",";
|
||||||
first = false;
|
first = false;
|
||||||
std::string url = label.empty()
|
std::string url = label.empty()
|
||||||
? std::format("{}.wasm", cfg.outputName)
|
? std::format("{}.wasm", cfg.outputName)
|
||||||
: std::format("{}.{}.wasm", cfg.outputName, label);
|
: std::format("{}.{}.wasm", cfg.outputName, label);
|
||||||
std::vector<std::string> probes = ownProbes;
|
|
||||||
probes.insert(probes.end(), WasmBaselineProbes.begin(), WasmBaselineProbes.end());
|
|
||||||
v << "{\"label\":\"" << jsonEscape(label) << "\""
|
v << "{\"label\":\"" << jsonEscape(label) << "\""
|
||||||
<< ",\"url\":\"" << jsonEscape(url) << "\""
|
<< ",\"url\":\"" << jsonEscape(url) << "\""
|
||||||
<< ",\"probes\":[";
|
<< ",\"probes\":[";
|
||||||
|
|
|
||||||
|
|
@ -818,12 +818,19 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||||
std::string sysrootFlag = config.sysroot.empty()
|
std::string sysrootFlag = config.sysroot.empty()
|
||||||
? std::string()
|
? std::string()
|
||||||
: std::format(" --sysroot={}", config.sysroot);
|
: std::format(" --sysroot={}", config.sysroot);
|
||||||
// wasm32 rejects -march, and the std module has to be compiled the way
|
// wasm32 rejects -march. wasi-libc++ headers require these baselines
|
||||||
// the units importing it are, a variant pass's flags (e.g.
|
// to compile: setjmp.h needs -mllvm -wasm-enable-sjlj (lowered to wasm
|
||||||
// -mrelaxed-simd) included, or the BMI is not feature-compatible.
|
// EH); signal.h requires the emulation define; and EH itself isn't
|
||||||
|
// wired up so -fno-exceptions stays.
|
||||||
std::string archFlags = isWasm
|
std::string archFlags = isWasm
|
||||||
? WasmCompileFlags(config)
|
? std::string(" -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL")
|
||||||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||||
|
// Match the variant codegen flags the consuming TUs are built with
|
||||||
|
// (e.g. -mrelaxed-simd) so the std BMI is feature-compatible. wasm only;
|
||||||
|
// wasmVariantFlags is empty on every other path.
|
||||||
|
if (isWasm) {
|
||||||
|
for (const std::string& f : config.wasmVariantFlags) archFlags += std::format(" {}", f);
|
||||||
|
}
|
||||||
// non-wasm cross builds: the driver ranks the host toolchain's own
|
// non-wasm cross builds: the driver ranks the host toolchain's own
|
||||||
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
|
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
|
||||||
// the PCM would mix host libc++ with the foreign libc — force the
|
// the PCM would mix host libc++ with the foreign libc — force the
|
||||||
|
|
|
||||||
|
|
@ -11,35 +11,6 @@ import :Toolchain;
|
||||||
import :Agents;
|
import :Agents;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace Crafter {
|
|
||||||
// What every wasm32 translation unit is compiled with, C or C++, and the
|
|
||||||
// std module the C++ ones import. 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). SIMD128
|
|
||||||
// is older, but V8 and SpiderMonkey switch it off on x86 CPUs without
|
|
||||||
// SSE4.1, which virtual machines often present.
|
|
||||||
inline constexpr auto WasmBaselineProbes = std::to_array<std::string_view>({"simd", "exception-handling"});
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Crafter {
|
export namespace Crafter {
|
||||||
struct BuildResult {
|
struct BuildResult {
|
||||||
std::string result;
|
std::string result;
|
||||||
|
|
@ -67,10 +38,8 @@ export namespace Crafter {
|
||||||
// `label` emits an additional `outputName.<label>.wasm` alongside the
|
// `label` emits an additional `outputName.<label>.wasm` alongside the
|
||||||
// baseline `outputName.wasm`; `probes` names the runtime feature-detect
|
// baseline `outputName.wasm`; `probes` names the runtime feature-detect
|
||||||
// checks (see wasi-runtime/runtime.js's probe registry) that must all pass
|
// checks (see wasi-runtime/runtime.js's probe registry) that must all pass
|
||||||
// for a browser to be served this variant, on top of WasmBaselineProbes,
|
// for a browser to be served this variant. The first variant whose probes
|
||||||
// which every build needs. The first variant whose probes pass wins; the
|
// pass wins; the baseline is the universal fallback.
|
||||||
// baseline is the fallback, and an engine that fails even its probes gets
|
|
||||||
// an error naming them.
|
|
||||||
struct WasmVariant {
|
struct WasmVariant {
|
||||||
std::string label;
|
std::string label;
|
||||||
std::vector<std::string> flags;
|
std::vector<std::string> flags;
|
||||||
|
|
@ -617,16 +586,6 @@ export namespace Crafter {
|
||||||
compileKey += "|W:";
|
compileKey += "|W:";
|
||||||
compileKey += f;
|
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
|
// Sorted by ApplyStandardArgs, so flag order on the command line
|
||||||
// doesn't split the cache.
|
// doesn't split the cache.
|
||||||
for (const std::string& a : projectArgs) {
|
for (const std::string& a : projectArgs) {
|
||||||
|
|
@ -663,8 +622,7 @@ export namespace Crafter {
|
||||||
|
|
||||||
// Directory holding the std module PCM for this Configuration. Keyed on
|
// 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
|
// target+march because a BMI is only loadable by a TU with matching target
|
||||||
// features, and suffixed with the wasm flags (WasmBaselineFlags, then the
|
// features, and suffixed with the wasm variant flags for the same reason.
|
||||||
// variant's) for the same reason.
|
|
||||||
fs::path StdPcmDir(const Configuration& config);
|
fs::path StdPcmDir(const Configuration& config);
|
||||||
|
|
||||||
// Where Build leaves this Configuration's linked artifact: the executable
|
// Where Build leaves this Configuration's linked artifact: the executable
|
||||||
|
|
@ -738,11 +696,7 @@ export namespace Crafter {
|
||||||
// open it). Opt-in: WASI builds destined for wasmtime/wasmer don't need
|
// open it). Opt-in: WASI builds destined for wasmtime/wasmer don't need
|
||||||
// this and shouldn't carry the extra files. Call from project.cpp after
|
// this and shouldn't carry the extra files. Call from project.cpp after
|
||||||
// outputName is set; index.html is generated against the current
|
// outputName is set; index.html is generated against the current
|
||||||
// outputName so renaming the binary later requires another call. In that
|
// outputName so renaming the binary later requires another call.
|
||||||
// runtime exit and _Exit end the program without running any more of it;
|
|
||||||
// a program that hands the thread to the browser and keeps serving calls
|
|
||||||
// parks instead, through the crafter_runtime.park import (runtime.js,
|
|
||||||
// PROGRAM_END).
|
|
||||||
void EnableWasiBrowserRuntime(Configuration& cfg);
|
void EnableWasiBrowserRuntime(Configuration& cfg);
|
||||||
|
|
||||||
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
|
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
|
||||||
|
|
@ -751,8 +705,7 @@ export namespace Crafter {
|
||||||
// it only to engines that report relaxed-SIMD support (Chrome 114+, Firefox
|
// it only to engines that report relaxed-SIMD support (Chrome 114+, Firefox
|
||||||
// 120+) and falls back to the baseline outputName.wasm everywhere else
|
// 120+) and falls back to the baseline outputName.wasm everywhere else
|
||||||
// (e.g. Safari, which still gates relaxed SIMD behind a flag as of mid
|
// (e.g. Safari, which still gates relaxed SIMD behind a flag as of mid
|
||||||
// 2026). Both need what WasmBaselineProbes names, so an engine without
|
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
||||||
// exception handling gets neither but an error saying so. Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
|
||||||
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
||||||
// time (no wasm is produced), but harmless to declare unconditionally.
|
// time (no wasm is produced), but harmless to declare unconditionally.
|
||||||
void EnableWasiRelaxedSimdVariant(Configuration& cfg);
|
void EnableWasiRelaxedSimdVariant(Configuration& cfg);
|
||||||
|
|
@ -786,11 +739,3 @@ export namespace Crafter {
|
||||||
// for-arg-in-args loop.
|
// for-arg-in-args loop.
|
||||||
ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
|
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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -135,22 +135,11 @@ modules:
|
||||||
- -DWASI_SDK_INSTALL_TO_CLANG_RESOURCE_DIR=ON
|
- -DWASI_SDK_INSTALL_TO_CLANG_RESOURCE_DIR=ON
|
||||||
- -DWASI_SDK_TARGETS=wasm32-wasip1;wasm32-wasip1-threads
|
- -DWASI_SDK_TARGETS=wasm32-wasip1;wasm32-wasip1-threads
|
||||||
- -DWASI_SDK_INCLUDE_TESTS=OFF
|
- -DWASI_SDK_INCLUDE_TESTS=OFF
|
||||||
# crafter-build compiles every wasm unit with C++ exceptions and links
|
|
||||||
# the libc++ and libunwind built with them, from lib/<triple>/eh. The
|
|
||||||
# default is DUAL only when the driving clang is 23 or newer and OFF
|
|
||||||
# below that; said outright, an LLVM downgrade cannot quietly ship a
|
|
||||||
# sysroot that no wasm build links against.
|
|
||||||
- -DWASI_SDK_EXCEPTIONS=DUAL
|
|
||||||
- -DCMAKE_INSTALL_PREFIX=/app
|
- -DCMAKE_INSTALL_PREFIX=/app
|
||||||
post-install:
|
post-install:
|
||||||
# wasi-sdk installs into <prefix>/share/wasi-sysroot; crafter-build
|
# wasi-sdk installs into <prefix>/share/wasi-sysroot; crafter-build
|
||||||
# looks there (Crafter.Build:Toolchain), so nothing to move.
|
# looks there (Crafter.Build:Toolchain), so nothing to move.
|
||||||
- test -f /app/share/wasi-sysroot/share/libc++/v1/std.cppm
|
- test -f /app/share/wasi-sysroot/share/libc++/v1/std.cppm
|
||||||
# The wasm tests skip when these are missing, as on a distro sysroot,
|
|
||||||
# so the Flatpak checks for them itself: without them its CI would
|
|
||||||
# pass while every wasm build failed.
|
|
||||||
- test -f /app/share/wasi-sysroot/lib/wasm32-wasip1/eh/libunwind.a
|
|
||||||
- test -f /app/share/wasi-sysroot/lib/wasm32-wasip1-threads/eh/libunwind.a
|
|
||||||
sources:
|
sources:
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/WebAssembly/wasi-sdk.git
|
url: https://github.com/WebAssembly/wasi-sdk.git
|
||||||
|
|
|
||||||
|
|
@ -266,12 +266,8 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("SysrootCompileFlags").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("SysrootCompileFlags").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasiBrowserRuntimeExit").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasmHelloRuns").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasmHelloRuns").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasmExceptions").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("WasiSysrootWithoutExceptions").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("WasmBuildRefusals").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
||||||
// LoadProject dlopens the synthesized project.so, which references
|
// LoadProject dlopens the synthesized project.so, which references
|
||||||
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,7 @@ int main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// With no declared variants, variants.json is a single baseline entry
|
// With no declared variants, variants.json is a single baseline entry
|
||||||
// pointing at outputName.wasm, with only the probes for what every wasm
|
// pointing at outputName.wasm with no probes.
|
||||||
// build needs, SIMD128 and exception handling, so that runtime.js can
|
|
||||||
// turn an engine without them away with a message rather than a
|
|
||||||
// CompileError.
|
|
||||||
{
|
{
|
||||||
std::ifstream vin(*variants);
|
std::ifstream vin(*variants);
|
||||||
std::string vjson{std::istreambuf_iterator<char>(vin), std::istreambuf_iterator<char>()};
|
std::string vjson{std::istreambuf_iterator<char>(vin), std::istreambuf_iterator<char>()};
|
||||||
|
|
@ -98,10 +95,6 @@ int main() {
|
||||||
std::println(std::cerr, "variants.json missing baseline url: {}", vjson);
|
std::println(std::cerr, "variants.json missing baseline url: {}", vjson);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (vjson.find("\"probes\":[\"simd\",\"exception-handling\"]") == std::string::npos) {
|
|
||||||
std::println(std::cerr, "variants.json baseline does not probe for SIMD and exception handling: {}", vjson);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (vjson.find("relaxed-simd") != std::string::npos) {
|
if (vjson.find("relaxed-simd") != std::string::npos) {
|
||||||
std::println(std::cerr, "variants.json unexpectedly mentions relaxed-simd: {}", vjson);
|
std::println(std::cerr, "variants.json unexpectedly mentions relaxed-simd: {}", vjson);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
// Runs wasi-runtime/runtime.js (argv[2], copied to a .mjs so that node loads
|
|
||||||
// it as a module) once per scenario against the module assembled below, and
|
|
||||||
// prints one line per scenario for the test to check. Between _start and the
|
|
||||||
// call that ends it, the module has the frames C++ built with
|
|
||||||
// -fwasm-exceptions puts there: a destructor (catch_all_ref, then
|
|
||||||
// throw_ref), a catch (...) (catch_all) and a noexcept function (catch_all,
|
|
||||||
// then std::terminate). Each reports through env.trace when it runs, and on
|
|
||||||
// the way out of an exit or a park none of them may.
|
|
||||||
import { pathToFileURL } from "node:url";
|
|
||||||
|
|
||||||
const uleb = (n) => {
|
|
||||||
const out = [];
|
|
||||||
do {
|
|
||||||
const byte = n & 0x7f;
|
|
||||||
n >>>= 7;
|
|
||||||
out.push(n ? byte | 0x80 : byte);
|
|
||||||
} while (n);
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
const sleb = (n) => {
|
|
||||||
const out = [];
|
|
||||||
for (;;) {
|
|
||||||
const byte = n & 0x7f;
|
|
||||||
n >>= 7;
|
|
||||||
const done = (n === 0 && !(byte & 0x40)) || (n === -1 && (byte & 0x40));
|
|
||||||
out.push(done ? byte : byte | 0x80);
|
|
||||||
if (done) return out;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const vec = (items) => [...uleb(items.length), ...items.flat()];
|
|
||||||
const name = (text) => vec([...text].map((c) => [c.charCodeAt(0)]));
|
|
||||||
const section = (id, items) => {
|
|
||||||
const bytes = vec(items);
|
|
||||||
return [id, ...uleb(bytes.length), ...bytes];
|
|
||||||
};
|
|
||||||
const UNREACHABLE = 0x00, BLOCK = 0x02, IF = 0x04, ELSE = 0x05, THROW = 0x08, THROW_REF = 0x0a, END = 0x0b;
|
|
||||||
const RETURN = 0x0f, CALL = 0x10, TRY_TABLE = 0x1f, LOCAL_GET = 0x20, LOCAL_SET = 0x21, GLOBAL_GET = 0x23;
|
|
||||||
const GLOBAL_SET = 0x24, I32_CONST = 0x41, I32_EQ = 0x46;
|
|
||||||
const I32 = 0x7f, EXNREF = 0x69, NO_RESULT = 0x40, FUNC_TYPE = 0x60, CATCH_ALL = 0x02, CATCH_ALL_REF = 0x03;
|
|
||||||
const code = (locals, instructions) => {
|
|
||||||
const bytes = [...vec(locals), ...instructions, END];
|
|
||||||
return [...uleb(bytes.length), ...bytes];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Types, imported functions, then the module's own.
|
|
||||||
const TAKES_I32 = 0, NOTHING = 1, GIVES_I32 = 2;
|
|
||||||
const PROC_EXIT = 0, PARK = 1, SCENARIO = 2, TRACE = 3;
|
|
||||||
const END_WITH = 4, NOEXCEPT = 5, CATCHES_ALL = 6, CLEANS_UP = 7, START = 8, FRAME = 9, QUIT = 10;
|
|
||||||
// What env.trace reports.
|
|
||||||
const DESTRUCTOR = 1, CATCH = 2, TERMINATE = 3, AFTER_PARK = 4;
|
|
||||||
// What env.scenario selects in _start.
|
|
||||||
const EXIT = 1, PARK_MAIN = 2, THROW_UNCAUGHT = 4, CRASH = 5;
|
|
||||||
const PARK_CODE = -1;
|
|
||||||
const ifScenario = (scenario, instructions) => [LOCAL_GET, 0, I32_CONST, ...sleb(scenario), I32_EQ, IF, NO_RESULT, ...instructions, END];
|
|
||||||
|
|
||||||
const module = new Uint8Array([
|
|
||||||
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
||||||
...section(1, [
|
|
||||||
[FUNC_TYPE, ...vec([[I32]]), ...vec([])],
|
|
||||||
[FUNC_TYPE, ...vec([]), ...vec([])],
|
|
||||||
[FUNC_TYPE, ...vec([]), ...vec([[I32]])],
|
|
||||||
]),
|
|
||||||
...section(2, [
|
|
||||||
[...name("wasi_snapshot_preview1"), ...name("proc_exit"), 0x00, TAKES_I32],
|
|
||||||
[...name("crafter_runtime"), ...name("park"), 0x00, NOTHING],
|
|
||||||
[...name("env"), ...name("scenario"), 0x00, GIVES_I32],
|
|
||||||
[...name("env"), ...name("trace"), 0x00, TAKES_I32],
|
|
||||||
]),
|
|
||||||
...section(3, [[TAKES_I32], [TAKES_I32], [TAKES_I32], [TAKES_I32], [NOTHING], [GIVES_I32], [TAKES_I32]]),
|
|
||||||
...section(5, [[0x00, 0x01]]),
|
|
||||||
// One tag, for the exception nothing catches.
|
|
||||||
...section(13, [[0x00, TAKES_I32]]),
|
|
||||||
...section(6, [[I32, 0x01, I32_CONST, 0x00, END]]),
|
|
||||||
...section(7, [
|
|
||||||
[...name("memory"), 0x02, 0],
|
|
||||||
[...name("_start"), 0x00, START],
|
|
||||||
[...name("frame"), 0x00, FRAME],
|
|
||||||
[...name("quit"), 0x00, QUIT],
|
|
||||||
]),
|
|
||||||
...section(10, [
|
|
||||||
// END_WITH(code): park for PARK_CODE, else proc_exit(code).
|
|
||||||
code([], [LOCAL_GET, 0, I32_CONST, ...sleb(PARK_CODE), I32_EQ, IF, NO_RESULT, CALL, PARK, ELSE, LOCAL_GET, 0, CALL, PROC_EXIT, END]),
|
|
||||||
// NOEXCEPT(code): anything thrown through it is std::terminate.
|
|
||||||
code([], [
|
|
||||||
BLOCK, NO_RESULT, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL, 0]]), LOCAL_GET, 0, CALL, END_WITH, END, RETURN, END,
|
|
||||||
I32_CONST, TERMINATE, CALL, TRACE, UNREACHABLE,
|
|
||||||
]),
|
|
||||||
// CATCHES_ALL(code): try { NOEXCEPT(code); } catch (...) {}
|
|
||||||
code([], [
|
|
||||||
BLOCK, NO_RESULT, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL, 0]]), LOCAL_GET, 0, CALL, NOEXCEPT, END, RETURN, END,
|
|
||||||
I32_CONST, CATCH, CALL, TRACE,
|
|
||||||
]),
|
|
||||||
// CLEANS_UP(code): a frame with a destructor around CATCHES_ALL(code).
|
|
||||||
code([[...uleb(1), EXNREF]], [
|
|
||||||
BLOCK, EXNREF, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL_REF, 0]]), LOCAL_GET, 0, CALL, CATCHES_ALL, END, RETURN, END,
|
|
||||||
LOCAL_SET, 1, I32_CONST, DESTRUCTOR, CALL, TRACE, LOCAL_GET, 1, THROW_REF,
|
|
||||||
]),
|
|
||||||
// _start: what env.scenario says; anything else returns from main.
|
|
||||||
code([[...uleb(1), I32]], [
|
|
||||||
CALL, SCENARIO, LOCAL_SET, 0,
|
|
||||||
...ifScenario(EXIT, [I32_CONST, 3, CALL, CLEANS_UP]),
|
|
||||||
...ifScenario(PARK_MAIN, [I32_CONST, 42, GLOBAL_SET, 0, I32_CONST, ...sleb(PARK_CODE), CALL, CLEANS_UP, I32_CONST, AFTER_PARK, CALL, TRACE]),
|
|
||||||
...ifScenario(THROW_UNCAUGHT, [I32_CONST, 9, THROW, 0]),
|
|
||||||
...ifScenario(CRASH, [UNREACHABLE]),
|
|
||||||
]),
|
|
||||||
// frame(): the state _start left, as a later frame callback reads it.
|
|
||||||
code([], [GLOBAL_GET, 0]),
|
|
||||||
// quit(code): an exit from a later call, through the same frames.
|
|
||||||
code([], [LOCAL_GET, 0, CALL, CLEANS_UP]),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// The standardized encoding (try_table) is what the module and runtime.js
|
|
||||||
// both depend on; an older node has only the legacy one.
|
|
||||||
const exnrefProbe = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 8, 1, 6, 0, 31, 64, 0, 11, 11]);
|
|
||||||
if (!WebAssembly.validate(exnrefProbe)) {
|
|
||||||
console.log("SKIP: this node has no WebAssembly exception handling");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
if (!WebAssembly.validate(module)) {
|
|
||||||
console.log("FAIL: the fixture module does not validate");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// What runtime.js expects of a page: no variants.json or files.json, so it
|
|
||||||
// loads CRAFTER_WASM_URL.
|
|
||||||
globalThis.window = globalThis;
|
|
||||||
globalThis.addEventListener = () => {};
|
|
||||||
window.CRAFTER_WASM_URL = "fixture.wasm";
|
|
||||||
globalThis.fetch = async (url) => {
|
|
||||||
if (String(url).split("?")[0] === "fixture.wasm") return new Response(module, { headers: { "Content-Type": "application/wasm" } });
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const runtime = pathToFileURL(process.argv[2]).href;
|
|
||||||
const run = async (label, scenario, after = () => "") => {
|
|
||||||
const trace = [];
|
|
||||||
window.crafter_webbuild_env = { scenario: () => scenario, trace: (what) => { trace.push(what); } };
|
|
||||||
delete window.crafter_wasi;
|
|
||||||
let outcome = "ended";
|
|
||||||
try {
|
|
||||||
// A query makes it a module of its own, run afresh.
|
|
||||||
await import(`${runtime}?scenario=${scenario}`);
|
|
||||||
} catch (e) {
|
|
||||||
outcome = `rejected with ${e?.constructor?.name}`;
|
|
||||||
}
|
|
||||||
const wasi = window.crafter_wasi;
|
|
||||||
console.log(`${label}: ${outcome}, exitCode=${wasi?.exitCode} parked=${wasi?.parked} trace=[${trace}]${after(wasi, trace)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
await run("exit", EXIT);
|
|
||||||
await run("park", PARK_MAIN, (wasi, trace) => {
|
|
||||||
const frame = wasi.instance.exports.frame();
|
|
||||||
let quit = "returned";
|
|
||||||
try {
|
|
||||||
wasi.instance.exports.quit(7);
|
|
||||||
} catch (e) {
|
|
||||||
quit = `threw ${e.constructor.name}`;
|
|
||||||
}
|
|
||||||
return `; frame()=${frame}; quit(7) ${quit}, exitCode=${wasi.exitCode} trace=[${trace}]`;
|
|
||||||
});
|
|
||||||
await run("return", 3);
|
|
||||||
await run("uncaught exception", THROW_UNCAUGHT);
|
|
||||||
await run("crash", CRASH);
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
||||||
// The browser runtime has to end a call into the program without running any
|
|
||||||
// more of it: when the program exits (exit, _Exit, a nonzero return from
|
|
||||||
// main), and when it parks to hand the thread to the browser for good, which
|
|
||||||
// Crafter::Window::StartSync does on DOM. runtime.js used to throw a
|
|
||||||
// JavaScript error out of proc_exit, and a program built with
|
|
||||||
// -fwasm-exceptions catches that. An exit then ran the destructors of every
|
|
||||||
// frame it left, a catch (...) swallowed it and carried on, and a noexcept
|
|
||||||
// function turned it into std::terminate, while the page went on calling
|
|
||||||
// into a program whose state those destructors had torn down.
|
|
||||||
//
|
|
||||||
// fixture/harness.mjs runs the real runtime.js under node against a module
|
|
||||||
// with the frames that catch between _start and the call that ends it: a
|
|
||||||
// destructor, a catch (...) and a noexcept function, each reporting when it
|
|
||||||
// runs. It checks that an exit runs none of them and records its status;
|
|
||||||
// that a park runs none of them either, nor the code after it, and leaves the
|
|
||||||
// program callable with its state intact; that an exit from a later call
|
|
||||||
// ends the program too; that returning from main ends it with status 0; and
|
|
||||||
// that a crash or an exception nothing caught still surfaces as an error.
|
|
||||||
// Skips without node, or with a node whose engine lacks exception handling.
|
|
||||||
// The Flatpak has no node, so this one runs in the native loop.
|
|
||||||
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() {
|
|
||||||
if (std::system("which node > /dev/null 2>&1") != 0) {
|
|
||||||
std::println(std::cerr, "SKIP: node not on PATH");
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-wasi-browser-runtime-exit";
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
// node runs a .js file as a CommonJS script, which cannot await at its
|
|
||||||
// top level the way runtime.js does.
|
|
||||||
const fs::path runtime = scratch / "runtime.mjs";
|
|
||||||
fs::copy_file(fs::current_path() / "wasi-runtime" / "runtime.js", runtime);
|
|
||||||
const fs::path harness = fs::current_path() / "tests" / "WasiBrowserRuntimeExit" / "fixture" / "harness.mjs";
|
|
||||||
|
|
||||||
const auto run = RunCommandWithTimeout(std::format("node {} {}", ShellQuote(harness.string()), ShellQuote(runtime.string())), std::chrono::seconds(60));
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
if (run.output.contains("SKIP:")) {
|
|
||||||
std::println(std::cerr, "{}", run.output);
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
Check(run.exitCode == 0, std::format("the harness ran to the end (exit {})", run.exitCode));
|
|
||||||
Check(run.output.contains("exit: ended, exitCode=3 parked=false trace=[]"), "an exit runs no destructor, catch (...) or terminate, and its status is recorded");
|
|
||||||
Check(run.output.contains("park: ended, exitCode=null parked=true trace=[]; frame()=42"), "a park runs none of them nor the code after it, and the program stays callable with its state");
|
|
||||||
Check(run.output.contains("quit(7) threw RuntimeError, exitCode=7 trace=[]"), "an exit from a later call ends the program the same way");
|
|
||||||
Check(run.output.contains("return: ended, exitCode=0 parked=false trace=[]"), "returning from main ends the program with status 0");
|
|
||||||
Check(run.output.contains("uncaught exception: rejected with Exception, exitCode=null"), "an exception nothing caught is not taken for an exit");
|
|
||||||
Check(run.output.contains("crash: rejected with RuntimeError, exitCode=null"), "a crash is not taken for an exit");
|
|
||||||
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed; harness output:\n{}", failures, run.output);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
||||||
// A WASI sysroot without exception support has to stop a wasm build with one
|
|
||||||
// line that says so. wasm builds compile with -fwasm-exceptions and link the
|
|
||||||
// libc++ and libunwind that wasi-sdk 33 and later ship under lib/<triple>/eh;
|
|
||||||
// a sysroot that lacks them (an older wasi-sdk, one built without its
|
|
||||||
// exceptions option) otherwise compiles and then fails the first link with
|
|
||||||
// "unable to find library -lunwind", which does not say why it was asked
|
|
||||||
// for. A sysroot that is not there at all, or has nothing for the target,
|
|
||||||
// gets a message of its own rather than that one. The checks run before
|
|
||||||
// anything is made, so a refused build leaves no bin/ behind, and they need
|
|
||||||
// no working wasm toolchain.
|
|
||||||
//
|
|
||||||
// A scratch sysroot is taken through these states: missing; without
|
|
||||||
// lib/wasm32-wasip1; without libunwind, for an executable and for a static
|
|
||||||
// library, which links nothing and passes; with libunwind under eh/; with
|
|
||||||
// libunwind in the triple's own directory, which the driver searches after
|
|
||||||
// eh/. The cases that pass the check fail later on the sysroot's empty std
|
|
||||||
// module source, which is not what is under test. The cache is redirected
|
|
||||||
// into the scratch directory as well: the std PCM cache is not keyed on the
|
|
||||||
// sysroot, and a PCM built from the fake source must not land where the real
|
|
||||||
// wasm tests read theirs.
|
|
||||||
|
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildResult BuildAgainst(const fs::path& project, const fs::path& sysroot, ConfigurationType type = ConfigurationType::Executable) {
|
|
||||||
Configuration app;
|
|
||||||
app.path = project;
|
|
||||||
app.name = "hello";
|
|
||||||
app.outputName = "hello";
|
|
||||||
app.target = "wasm32-wasip1";
|
|
||||||
app.sysroot = sysroot.string();
|
|
||||||
app.type = type;
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
return Build(app, depResults, depMutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
// Build() reports a missing compiler before it gets to the sysroot.
|
|
||||||
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
|
|
||||||
std::println(std::cerr, "SKIP: {}", tc.error);
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-wasi-sysroot-without-exceptions";
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
|
||||||
const fs::path project = scratch / "project";
|
|
||||||
fs::copy(fs::current_path() / "tests" / "WasiSysrootWithoutExceptions" / "fixture", project, fs::copy_options::recursive);
|
|
||||||
|
|
||||||
const fs::path sysroot = scratch / "sysroot";
|
|
||||||
const fs::path libDir = sysroot / "lib" / "wasm32-wasip1";
|
|
||||||
{
|
|
||||||
const BuildResult r = BuildAgainst(project, sysroot);
|
|
||||||
Check(r.result.contains(std::format("no WASI sysroot at {}", sysroot.string())), std::format("a missing sysroot is named as missing: {}", r.result));
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::create_directories(sysroot / "share" / "libc++" / "v1");
|
|
||||||
std::ofstream(sysroot / "share" / "libc++" / "v1" / "std.cppm");
|
|
||||||
{
|
|
||||||
const BuildResult r = BuildAgainst(project, sysroot);
|
|
||||||
Check(r.result.contains(std::format("has nothing for wasm32-wasip1 (no {})", libDir.string())), std::format("a sysroot without the target's directory says so: {}", r.result));
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::create_directories(libDir / "eh");
|
|
||||||
constexpr std::string_view missing = "has no C++ exception support";
|
|
||||||
{
|
|
||||||
const BuildResult r = BuildAgainst(project, sysroot);
|
|
||||||
Check(r.result.contains(missing), std::format("without libunwind the build stops on the sysroot: {}", r.result));
|
|
||||||
Check(r.result.contains((libDir / "eh" / "libunwind.a").string()), std::format("the message names the library it looked for: {}", r.result));
|
|
||||||
Check(!fs::exists(project / "bin") && !fs::exists(project / "build"), "a refused build leaves no bin/ or build/ behind");
|
|
||||||
}
|
|
||||||
Check(!BuildAgainst(project, sysroot, ConfigurationType::LibraryStatic).result.contains(missing), "a static library links nothing and passes without libunwind");
|
|
||||||
|
|
||||||
std::ofstream(libDir / "eh" / "libunwind.a");
|
|
||||||
Check(!BuildAgainst(project, sysroot).result.contains(missing), "libunwind under eh/ passes the check");
|
|
||||||
|
|
||||||
fs::remove(libDir / "eh" / "libunwind.a");
|
|
||||||
std::ofstream(libDir / "libunwind.a");
|
|
||||||
Check(!BuildAgainst(project, sysroot).result.contains(missing), "libunwind in the triple's directory passes the check");
|
|
||||||
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
||||||
// What a wasm32 build refuses before it starts, now that every wasm unit has
|
|
||||||
// C++ exceptions, each with a message naming the cause:
|
|
||||||
// - -flto in compileFlags or linkFlags. LTO code generation drops the
|
|
||||||
// exception handling, and the build used to succeed with every throw
|
|
||||||
// escaping its catch (WebAssembly/wasi-sdk#629).
|
|
||||||
// - -fno-exceptions. The std module is built with exceptions, and every
|
|
||||||
// `import std;` failed on a BMI configuration mismatch.
|
|
||||||
// - A shared library. The exception-enabled libc++ is static archives
|
|
||||||
// only, and the link failed on a relocation that wants -fPIC.
|
|
||||||
// None of them leaves bin/ or build/ behind, and none needs a working wasm
|
|
||||||
// toolchain. The sysroot given does not exist, so a refusal that did not
|
|
||||||
// fire would show up as the missing sysroot instead.
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Refusal {
|
|
||||||
std::string_view what;
|
|
||||||
std::vector<std::string> compileFlags;
|
|
||||||
std::vector<std::string> linkFlags;
|
|
||||||
ConfigurationType type;
|
|
||||||
std::string_view expected;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
// Build() reports a missing compiler before it gets to any of this.
|
|
||||||
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
|
|
||||||
std::println(std::cerr, "SKIP: {}", tc.error);
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-wasm-build-refusals";
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
const fs::path project = scratch / "project";
|
|
||||||
fs::copy(fs::current_path() / "tests" / "WasmBuildRefusals" / "fixture", project, fs::copy_options::recursive);
|
|
||||||
|
|
||||||
const std::array refusals = {
|
|
||||||
Refusal{ "-flto in compileFlags", { "-flto=thin" }, {}, ConfigurationType::Executable, "-flto=thin is not supported on wasm32: LTO code generation drops C++ exception handling" },
|
|
||||||
Refusal{ "-flto in linkFlags", {}, { "-flto" }, ConfigurationType::Executable, "-flto is not supported on wasm32" },
|
|
||||||
Refusal{ "-fno-exceptions", { "-fno-exceptions" }, {}, ConfigurationType::Executable, "-fno-exceptions is not supported on wasm32" },
|
|
||||||
Refusal{ "a shared library", {}, {}, ConfigurationType::LibraryDynamic, "wasm32 shared libraries are not supported" },
|
|
||||||
};
|
|
||||||
for (const Refusal& refusal : refusals) {
|
|
||||||
Configuration app;
|
|
||||||
app.path = project;
|
|
||||||
app.name = "hello";
|
|
||||||
app.outputName = "hello";
|
|
||||||
app.target = "wasm32-wasip1";
|
|
||||||
app.sysroot = (scratch / "no-sysroot").string();
|
|
||||||
app.type = refusal.type;
|
|
||||||
app.compileFlags = refusal.compileFlags;
|
|
||||||
app.linkFlags = refusal.linkFlags;
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
const BuildResult r = Build(app, depResults, depMutex);
|
|
||||||
Check(r.result.contains(refusal.expected), std::format("{} is refused with its cause: {}", refusal.what, r.result));
|
|
||||||
Check(!fs::exists(project / "bin") && !fs::exists(project / "build"), std::format("refusing {} leaves no bin/ or build/ behind", refusal.what));
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#include <setjmp.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
static jmp_buf target;
|
|
||||||
|
|
||||||
static void JumpBack(int32_t value) {
|
|
||||||
longjmp(target, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// setjmp's value may only be compared or switched on, never stored.
|
|
||||||
int32_t JumpRoundTrip(void) {
|
|
||||||
switch (setjmp(target)) {
|
|
||||||
case 0:
|
|
||||||
JumpBack(7);
|
|
||||||
return 0;
|
|
||||||
case 7:
|
|
||||||
return 7;
|
|
||||||
default:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
|
|
||||||
// jump.c, compiled as C.
|
|
||||||
extern "C" std::int32_t JumpRoundTrip();
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::int32_t unwound = 0;
|
|
||||||
|
|
||||||
struct Guard {
|
|
||||||
~Guard() { ++unwound; }
|
|
||||||
};
|
|
||||||
|
|
||||||
void Throw(std::int32_t depth) {
|
|
||||||
Guard guard;
|
|
||||||
if (depth == 0) throw std::runtime_error("thrown in wasm");
|
|
||||||
Throw(depth - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
// With an argument, nothing catches: the run has to fail.
|
|
||||||
if (argc > 1) Throw(0);
|
|
||||||
try {
|
|
||||||
Throw(3);
|
|
||||||
} catch (const std::runtime_error& e) {
|
|
||||||
std::println("caught: {} after {} destructors", e.what(), unwound);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const std::vector<std::int32_t> empty;
|
|
||||||
std::println("unreachable: {}", empty.at(1));
|
|
||||||
} catch (const std::out_of_range&) {
|
|
||||||
std::println("caught: out_of_range from libc++");
|
|
||||||
}
|
|
||||||
// vector::at throws from a header, compiled into this unit; stoi throws
|
|
||||||
// inside libc++.a, which therefore has to be the exception-enabled one.
|
|
||||||
try {
|
|
||||||
std::println("unreachable: {}", std::stoi("not a number"));
|
|
||||||
} catch (const std::invalid_argument&) {
|
|
||||||
std::println("caught: invalid_argument from inside libc++");
|
|
||||||
}
|
|
||||||
std::println("longjmp came back with {}", JumpRoundTrip());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
// 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<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
cfg.cFiles = { cfg.path / "jump" };
|
|
||||||
|
|
||||||
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));
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -11,15 +11,12 @@ using namespace Crafter;
|
||||||
// wasmtime. The other wasm tests only inspect generated files; this is the
|
// wasmtime. The other wasm tests only inspect generated files; this is the
|
||||||
// one that proves the sysroot, the std module for wasm and wasm-ld all work
|
// one that proves the sysroot, the std module for wasm and wasm-ld all work
|
||||||
// together. It skips when the sysroot or wasmtime is missing, so a
|
// together. It skips when the sysroot or wasmtime is missing, so a
|
||||||
// development box without them is not a failure; a sysroot without the
|
// development box without them is not a failure. Inside the Flatpak both are
|
||||||
// exception-enabled libraries counts as missing, since wasm builds link them
|
// bundled, and there the skip would be the failure to look for.
|
||||||
// (WasiSysrootWithoutExceptions covers that message). Inside the Flatpak both
|
|
||||||
// are bundled, and there the skip would be the failure to look for.
|
|
||||||
int main() {
|
int main() {
|
||||||
const Toolchain& tc = GetToolchain();
|
const Toolchain& tc = GetToolchain();
|
||||||
const fs::path libDir = tc.wasiSysroot / "lib" / "wasm32-wasip1";
|
if (!fs::exists(tc.wasiSysroot / "include")) {
|
||||||
if (!fs::exists(libDir / "eh" / "libunwind.a") && !fs::exists(libDir / "libunwind.a")) {
|
std::println(std::cerr, "SKIP: no WASI sysroot at {}", tc.wasiSysroot.string());
|
||||||
std::println(std::cerr, "SKIP: no exception-enabled WASI sysroot at {}", tc.wasiSysroot.string());
|
|
||||||
return 77;
|
return 77;
|
||||||
}
|
}
|
||||||
if (std::system("which wasmtime > /dev/null 2>&1") != 0) {
|
if (std::system("which wasmtime > /dev/null 2>&1") != 0) {
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,6 @@ int main() {
|
||||||
Check(j.find("\"label\":\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd label");
|
Check(j.find("\"label\":\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd label");
|
||||||
Check(j.find("\"wvhelper.relaxed-simd.wasm\"") != std::string::npos, "manifest has labelled url");
|
Check(j.find("\"wvhelper.relaxed-simd.wasm\"") != std::string::npos, "manifest has labelled url");
|
||||||
Check(j.find("\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd probe");
|
Check(j.find("\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd probe");
|
||||||
// Every file is built with SIMD128 and exceptions, so every entry
|
|
||||||
// needs them besides its own features; the baseline needs
|
|
||||||
// nothing else.
|
|
||||||
Check(j.find("\"probes\":[\"relaxed-simd\",\"simd\",\"exception-handling\"]") != std::string::npos, "relaxed-simd entry also probes for the baseline's features");
|
|
||||||
Check(j.find("\"url\":\"wvhelper.wasm\",\"probes\":[\"simd\",\"exception-handling\"]") != std::string::npos, "baseline entry probes for its own features only");
|
|
||||||
// baseline fallback entry.
|
// baseline fallback entry.
|
||||||
Check(j.find("\"wvhelper.wasm\"") != std::string::npos, "manifest has baseline url");
|
Check(j.find("\"wvhelper.wasm\"") != std::string::npos, "manifest has baseline url");
|
||||||
// The relaxed-simd variant must precede the baseline so the runtime
|
// The relaxed-simd variant must precede the baseline so the runtime
|
||||||
|
|
|
||||||
|
|
@ -176,53 +176,11 @@ function markDirty(path) {
|
||||||
scheduleFlush();
|
scheduleFlush();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── leaving the program ───────────────────────────────────────────────
|
|
||||||
// exit and _Exit (proc_exit) end the program. park hands the thread to the
|
|
||||||
// browser for good while the program goes on serving the calls bridges make
|
|
||||||
// into it (Crafter::Window::StartSync on DOM): the import
|
|
||||||
// crafter_runtime.park, `void park(void)`, which never returns. Either way
|
|
||||||
// the current call into the program has to end without running any more of
|
|
||||||
// its code: not the destructors of the frames it leaves, not a catch (...),
|
|
||||||
// not the std::terminate of a noexcept function. Built with
|
|
||||||
// -fwasm-exceptions, every one of those catches a JavaScript exception
|
|
||||||
// thrown through it, so the way out cannot be a throw from JS. A trap is
|
|
||||||
// the one thing wasm code cannot catch, and this module raises it in wasm
|
|
||||||
// itself, bound directly as the program's import; a trap raised from JS
|
|
||||||
// would depend on each engine keeping it uncatchable when it passes back
|
|
||||||
// into wasm. It reports first, through host.ended, which has returned by
|
|
||||||
// the time it traps:
|
|
||||||
// (import "host" "ended" (func $ended (param $how i32) (param $code i32)))
|
|
||||||
// (func (export "exit") (param $code i32)
|
|
||||||
// i32.const 1 local.get $code call $ended unreachable)
|
|
||||||
// (func (export "park")
|
|
||||||
// i32.const 2 i32.const 0 call $ended unreachable)
|
|
||||||
// The program's own state is left as it is, its shadow stack included, so
|
|
||||||
// the locals of a parked main stay valid for the calls that follow.
|
|
||||||
const PROGRAM_END = new Uint8Array([
|
|
||||||
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
||||||
0x01, 0x0d, 0x03, 0x60, 0x02, 0x7f, 0x7f, 0x00, 0x60, 0x01, 0x7f, 0x00, 0x60, 0x00, 0x00,
|
|
||||||
0x02, 0x0e, 0x01, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x05, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x00, 0x00,
|
|
||||||
0x03, 0x03, 0x02, 0x01, 0x02,
|
|
||||||
0x07, 0x0f, 0x02, 0x04, 0x65, 0x78, 0x69, 0x74, 0x00, 0x01, 0x04, 0x70, 0x61, 0x72, 0x6b, 0x00, 0x02,
|
|
||||||
0x0a, 0x15, 0x02,
|
|
||||||
0x09, 0x00, 0x41, 0x01, 0x20, 0x00, 0x10, 0x00, 0x00, 0x0b,
|
|
||||||
0x09, 0x00, 0x41, 0x02, 0x41, 0x00, 0x10, 0x00, 0x00, 0x0b,
|
|
||||||
]);
|
|
||||||
const ENDED_BY_EXIT = 1;
|
|
||||||
const ENDED_BY_PARK = 2;
|
|
||||||
|
|
||||||
class Wasi {
|
class Wasi {
|
||||||
#encodedStdin;
|
#encodedStdin;
|
||||||
#envEncodedStrings;
|
#envEncodedStrings;
|
||||||
#argEncodedStrings;
|
#argEncodedStrings;
|
||||||
instance;
|
instance;
|
||||||
// The program's exit status once it has ended, through exit or _Exit or
|
|
||||||
// by returning from main; null while it runs or is parked. A bridge that
|
|
||||||
// calls into the program on its own, such as a frame loop, stops once
|
|
||||||
// this is set: nothing of an ended program may run.
|
|
||||||
exitCode = null;
|
|
||||||
// True once the program has parked (see PROGRAM_END).
|
|
||||||
parked = false;
|
|
||||||
#fdTable;
|
#fdTable;
|
||||||
#nextFd;
|
#nextFd;
|
||||||
#decoder;
|
#decoder;
|
||||||
|
|
@ -236,27 +194,6 @@ class Wasi {
|
||||||
this.#nextFd = 100; // stay clear of stdio + preopens
|
this.#nextFd = 100; // stay clear of stdio + preopens
|
||||||
this.#decoder = new TextDecoder();
|
this.#decoder = new TextDecoder();
|
||||||
this.bind();
|
this.bind();
|
||||||
const end = new WebAssembly.Instance(new WebAssembly.Module(PROGRAM_END), {
|
|
||||||
host: { ended: (how, code) => this.ended(how, code) },
|
|
||||||
}).exports;
|
|
||||||
// Own properties, so instantiation takes these wasm functions as the
|
|
||||||
// program's imports rather than a JS method.
|
|
||||||
this.proc_exit = end.exit;
|
|
||||||
this.park = end.park;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by PROGRAM_END just before its trap ends the call into the
|
|
||||||
// program. An exit after a park ends the program all the same.
|
|
||||||
ended(how, code) {
|
|
||||||
if (how === ENDED_BY_PARK) {
|
|
||||||
this.parked = true;
|
|
||||||
} else if (how === ENDED_BY_EXIT) {
|
|
||||||
console.log(`[wasi] proc_exit(${code})`);
|
|
||||||
this.exitCode = code;
|
|
||||||
// The last chance for anything written this session to reach
|
|
||||||
// OPFS before the page is torn down.
|
|
||||||
opfsFlushAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bind() {
|
bind() {
|
||||||
|
|
@ -868,6 +805,22 @@ class Wasi {
|
||||||
sizePointerBuffer[0] = envByteLength;
|
sizePointerBuffer[0] = envByteLength;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
proc_exit(code) {
|
||||||
|
// Throw a sentinel so the wasm stack unwinds back to runtime.js
|
||||||
|
// WITHOUT executing the `unreachable` instruction wasi-libc emits
|
||||||
|
// after __wasi_proc_exit (which is declared noreturn). Trapping
|
||||||
|
// there would mark the wasm call as crashed and may interrupt any
|
||||||
|
// browser-side render loop that called _Exit on purpose (e.g.
|
||||||
|
// Crafter::Window::StartSync on DOM, which hands the loop to rAF
|
||||||
|
// and then exits main without running static destructors).
|
||||||
|
console.log(`[wasi] proc_exit(${code})`);
|
||||||
|
// Trigger a final flush so anything written this session has the
|
||||||
|
// best chance of reaching OPFS before the page is torn down.
|
||||||
|
opfsFlushAll();
|
||||||
|
const e = new Error(`wasi proc_exit(${code})`);
|
||||||
|
e.crafterWasiExit = code;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── wasm feature-detect probes ────────────────────────────────────────
|
// ─── wasm feature-detect probes ────────────────────────────────────────
|
||||||
|
|
@ -876,28 +829,21 @@ class Wasi {
|
||||||
// GoogleChromeLabs/wasm-feature-detect, inlined so we ship no dependency. A
|
// GoogleChromeLabs/wasm-feature-detect, inlined so we ship no dependency. A
|
||||||
// variant in variants.json lists the probe names that must all pass for that
|
// variant in variants.json lists the probe names that must all pass for that
|
||||||
// variant to be served; an unknown name is treated as unsupported (we fall
|
// variant to be served; an unknown name is treated as unsupported (we fall
|
||||||
// through to a less demanding variant, ultimately the baseline, and to an
|
// through to a less demanding variant, ultimately the baseline). Keep the keys
|
||||||
// error naming what is missing when not even the baseline fits). Keep the
|
// in sync with the `probes` a WasmVariant declares in Crafter.Build.
|
||||||
// keys in sync with the `probes` a WasmVariant declares in Crafter.Build, and
|
|
||||||
// with WasmBaselineProbes, which every entry carries.
|
|
||||||
const WASM_PROBES = {
|
const WASM_PROBES = {
|
||||||
// relaxed SIMD: i8x16.relaxed_swizzle (Chrome 114+, Firefox 120+; Safari
|
// relaxed SIMD: i8x16.relaxed_swizzle (Chrome 114+, Firefox 120+; Safari
|
||||||
// still flag-gated as of mid-2026).
|
// still flag-gated as of mid-2026).
|
||||||
"relaxed-simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])),
|
"relaxed-simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])),
|
||||||
// fixed-width SIMD128 (v128.const + i8x16.popcnt), which every build
|
// fixed-width SIMD128 (v128.const + i8x16.popcnt) — the current baseline,
|
||||||
// uses (-msimd128). V8 and SpiderMonkey switch it off on x86 CPUs without
|
// here for completeness / future baseline shifts.
|
||||||
// SSE4.1, so it is a baseline probe even though every engine with
|
|
||||||
// exception handling has it.
|
|
||||||
"simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])),
|
"simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])),
|
||||||
// tail calls (return_call).
|
// tail calls (return_call).
|
||||||
"tail-call": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,6,1,4,0,18,0,11])),
|
"tail-call": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,6,1,4,0,18,0,11])),
|
||||||
// bulk memory ops (memory.init).
|
// bulk memory ops (memory.init).
|
||||||
"bulk-memory": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),
|
"bulk-memory": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),
|
||||||
// exception handling in its standardized form (try_table, exnref; Chrome
|
// legacy exception handling (try/catch).
|
||||||
// 137, Firefox 131, Safari 18.4), which every crafter-build wasm uses for
|
"exception-handling": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,6,64,25,11,11])),
|
||||||
// C++ exceptions. Probing the legacy try/catch encoding instead says
|
|
||||||
// nothing about this one: V8 accepts it with exnref switched off.
|
|
||||||
"exception-handling": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,31,64,0,11,11])),
|
|
||||||
// threads/atomics — also requires SharedArrayBuffer (cross-origin isolated).
|
// threads/atomics — also requires SharedArrayBuffer (cross-origin isolated).
|
||||||
"threads": () => {
|
"threads": () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -920,9 +866,6 @@ function variantSupported(probes) {
|
||||||
|
|
||||||
// Pick the wasm URL: prefer the variants.json manifest (first entry whose
|
// Pick the wasm URL: prefer the variants.json manifest (first entry whose
|
||||||
// probes all pass), else fall back to the single baked window.CRAFTER_WASM_URL.
|
// probes all pass), else fall back to the single baked window.CRAFTER_WASM_URL.
|
||||||
// When the manifest has no entry this engine can run, not even the baseline,
|
|
||||||
// say which features it lacks: loading the baseline anyway would only fail
|
|
||||||
// with a CompileError about an unknown opcode.
|
|
||||||
async function pickWasmUrl() {
|
async function pickWasmUrl() {
|
||||||
let variants = null;
|
let variants = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -938,9 +881,6 @@ async function pickWasmUrl() {
|
||||||
return v.url;
|
return v.url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const baseline = variants[variants.length - 1];
|
|
||||||
const missing = (baseline.probes || []).filter((name) => !variantSupported([name]));
|
|
||||||
throw new Error(`[wasi] ${baseline.url} needs WebAssembly ${missing.join(", ")}, which this browser does not support; a current Chrome, Firefox or Safari does`);
|
|
||||||
}
|
}
|
||||||
return window.CRAFTER_WASM_URL;
|
return window.CRAFTER_WASM_URL;
|
||||||
}
|
}
|
||||||
|
|
@ -1030,7 +970,6 @@ if (window.crafter_webbuild_env_ready) {
|
||||||
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
|
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
|
||||||
wasi_snapshot_preview1: wasi,
|
wasi_snapshot_preview1: wasi,
|
||||||
env: window.crafter_webbuild_env,
|
env: window.crafter_webbuild_env,
|
||||||
crafter_runtime: { park: wasi.park },
|
|
||||||
});
|
});
|
||||||
wasi.instance = instance;
|
wasi.instance = instance;
|
||||||
// Alias under both names — `crafter_wasi` is the canonical, but the older
|
// Alias under both names — `crafter_wasi` is the canonical, but the older
|
||||||
|
|
@ -1041,12 +980,12 @@ window.crafter_webbuild_wasi = wasi;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
instance.exports._start();
|
instance.exports._start();
|
||||||
// main returned 0; any other status reaches proc_exit.
|
|
||||||
wasi.exitCode = 0;
|
|
||||||
opfsFlushAll();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// proc_exit and park end _start with PROGRAM_END's trap after saying
|
if (e && typeof e.crafterWasiExit === "number") {
|
||||||
// which they were. Any other error, a crash or a C++ exception that
|
// Clean exit from std::_Exit / __wasi_proc_exit. The wasm instance
|
||||||
// nothing caught, is the program failing.
|
// remains valid; any rAF callback registered before exit keeps
|
||||||
if (wasi.exitCode === null && !wasi.parked) throw e;
|
// running. (See Crafter::Window::StartSync DOM impl.)
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue