From f81b5630f5885e017865d3c9d5fbd3383490f0ee Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Thu, 25 Jun 2026 11:46:19 -0700 Subject: [PATCH 1/2] Build sseq_gui with panic=unwind Since this was merged wasm-bindgen supports panic=unwind: https://github.com/wasm-bindgen/wasm-bindgen/pull/4796 --- web_ext/sseq_gui/Makefile | 24 +++++++++++++++++++++--- web_ext/sseq_gui/README.md | 5 +++++ web_ext/sseq_gui/flake.nix | 8 ++++++++ web_ext/sseq_gui/src/wasm_bindings.rs | 17 +++++++++++++---- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/web_ext/sseq_gui/Makefile b/web_ext/sseq_gui/Makefile index 47f31b5740..f689266559 100644 --- a/web_ext/sseq_gui/Makefile +++ b/web_ext/sseq_gui/Makefile @@ -7,6 +7,13 @@ EXT = ../../ext/ EXT_SRC = $(EXT)/Cargo.toml $(shell find $(EXT)/src/) $(wildcard $(EXT)/crates/*/Cargo.tml) $(shell find $(EXT)/crates/*/src/) +# The prebuilt std for wasm32-unknown-unknown is compiled with `panic=abort`, +# so `-C panic=unwind` alone is silently ignored. To get real unwinding we must +# rebuild std with `-Z build-std` (nightly only) and enable the wasm +# exception-handling proposal. +WASM_RUSTFLAGS = -C panic=unwind -C target-feature=+exception-handling +WASM_BUILD_STD = -Z build-std=std,panic_unwind + export PATH := $(HOME)/.cargo/bin:$(PATH) lint: @@ -20,15 +27,26 @@ lint-wasm: setup-wasm: cargo install wasm-bindgen-cli --debug rustup target add wasm32-unknown-unknown + # rust-src is required by `-Z build-std` (see the WASM_BUILD_STD note above). + rustup component add rust-src $(WASM_LIB): Cargo.toml $(wildcard src/*) $(EXT_SRC) - cargo build --lib --target $(WASM_TARGET) --release + RUSTFLAGS="$$RUSTFLAGS $(WASM_RUSTFLAGS)" \ + cargo build --lib --target $(WASM_TARGET) --release $(WASM_BUILD_STD) $(WASM_FILE): $(WASM_LIB) wasm-bindgen --no-typescript --target no-modules --out-dir $(WASM_OUT) --out-name $(NAME)_wasm $(WASM_LIB) - wasm-opt -O3 $(WASM_FILE) -o $(WASM_FILE) || true + wasm-opt -O3 --enable-exception-handling $(WASM_FILE) -o $(WASM_FILE) || true + +# Assert that the wasm is actually built with unwinding support: a wasm module +# using the exception-handling proposal contains a `Tag` section. If unwinding +# silently regressed to `panic=abort` (e.g. build-std got dropped), this fails. +test-wasm-unwind: $(WASM_LIB) + wasm-objdump -h $(WASM_LIB) | grep -qi '^ *Tag ' \ + || { echo "ERROR: $(WASM_LIB) has no Tag section; panic=unwind is not in effect"; exit 1; } + @echo "OK: wasm built with exception-handling (panic=unwind)" -.PHONY: wasm serve-wasm clean-wasm clean dummy test selenium selenium-update +.PHONY: wasm serve-wasm clean-wasm clean dummy test selenium selenium-update test-wasm-unwind wasm: $(WASM_FILE) $(wildcard interface/*) $(wildcard wasm/*) $(wildcard $(EXT)/steenrod_modules/*) # Must be done in this order since both contain index.js and we want the wasm version diff --git a/web_ext/sseq_gui/README.md b/web_ext/sseq_gui/README.md index 44d1b33e33..b6a2c1f503 100644 --- a/web_ext/sseq_gui/README.md +++ b/web_ext/sseq_gui/README.md @@ -35,6 +35,11 @@ To setup the build environment, run make setup-wasm ``` +The wasm build rebuilds the standard library with `panic=unwind` (via +`-Z build-std`) so that Rust panics unwind into JavaScript exceptions instead of +aborting the whole module. This requires a **nightly** toolchain with the +`rust-src` component (installed by `make setup-wasm`). + Afterwards, build and serve with ```shell diff --git a/web_ext/sseq_gui/flake.nix b/web_ext/sseq_gui/flake.nix index e6af9fad2d..d9c6b96039 100644 --- a/web_ext/sseq_gui/flake.nix +++ b/web_ext/sseq_gui/flake.nix @@ -13,6 +13,10 @@ rustToolchain = fenixPkgs.combine [ super.defaultPackages.rustToolchain.${system} fenixPkgs.targets.wasm32-unknown-unknown.latest.toolchain + # rust-src is needed for `-Z build-std`, which we use to rebuild the + # standard library with `panic=unwind` for the wasm target (the + # prebuilt std ships as `panic=abort`). + fenixPkgs.complete.rust-src ]; pythonEnv = pkgs.python3.withPackages (ps: [ @@ -29,6 +33,9 @@ pythonEnv pkgs.openssl + # wabt provides wasm-objdump, used by `make test-wasm-unwind` to + # assert the wasm is actually built with unwinding support. + pkgs.wabt ] ++ super.defaultPackages.devTools.${system}; @@ -44,6 +51,7 @@ cargo install wasm-bindgen-cli --debug make lint-wasm make wasm + make test-wasm-unwind make serve-wasm & (sleep 1 && make selenium) diff --git a/web_ext/sseq_gui/src/wasm_bindings.rs b/web_ext/sseq_gui/src/wasm_bindings.rs index 51faaf9822..16bbec6409 100644 --- a/web_ext/sseq_gui/src/wasm_bindings.rs +++ b/web_ext/sseq_gui/src/wasm_bindings.rs @@ -1,3 +1,5 @@ +use std::panic::AssertUnwindSafe; + use js_sys::Function; use wasm_bindgen::prelude::*; @@ -22,14 +24,20 @@ impl Sender { #[wasm_bindgen] pub struct Resolution { - r: ResolutionManager, + // The manager holds `Arc`/`RwLock`/`DashMap` and so is not `RefUnwindSafe`. + // With `panic=unwind`, wasm-bindgen wraps every exported method in + // `catch_unwind` and therefore requires the exported struct to be + // `RefUnwindSafe`. A panic that escapes a method tears down this whole + // instance, so there are no surviving broken invariants to guard against; + // assert unwind safety to satisfy the bound. + r: AssertUnwindSafe, } #[wasm_bindgen] impl Resolution { pub fn new(f: Function) -> Self { Self { - r: ResolutionManager::new(Sender::new(f)), + r: AssertUnwindSafe(ResolutionManager::new(Sender::new(f))), } } @@ -45,14 +53,15 @@ impl Resolution { #[wasm_bindgen] pub struct Sseq { - s: SseqManager, + // See the note on `Resolution::r` for why this is `AssertUnwindSafe`. + s: AssertUnwindSafe, } #[wasm_bindgen] impl Sseq { pub fn new(f: Function) -> Self { Self { - s: SseqManager::new(Sender::new(f)), + s: AssertUnwindSafe(SseqManager::new(Sender::new(f))), } } From ebf288df79de9282e7ceb937f86b9e8a04255ece Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Thu, 25 Jun 2026 12:56:57 -0700 Subject: [PATCH 2/2] Build stable/beta wasm with panic=abort, deploy nightly panic=unwind Make panic=unwind opt-in via WASM_UNWIND=1 so the default wasm build still works on stable/beta. CI builds the compatibility (panic=abort) artifact on stable/beta and a dedicated nightly job builds the deployed artifact with panic=unwind, which the deploy job now publishes. --- .github/workflows/ext.yaml | 63 ++++++++++++++++++++++++++++++++++++-- web_ext/sseq_gui/Makefile | 35 +++++++++++++++------ web_ext/sseq_gui/README.md | 16 +++++++--- 3 files changed, 97 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ext.yaml b/.github/workflows/ext.yaml index 569743feeb..d3026ac60c 100644 --- a/.github/workflows/ext.yaml +++ b/.github/workflows/ext.yaml @@ -124,6 +124,9 @@ jobs: - name: Lint sseq_gui tests run: make -C web_ext/sseq_gui lint-selenium + # Build the wasm webserver with the default `panic=abort` configuration on + # stable/beta. This is the compatibility build; the deployed artifact is + # produced by the `webserver-deploy` job below with `panic=unwind`. webserver: if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository }} runs-on: ubuntu-latest @@ -134,7 +137,7 @@ jobs: strategy: matrix: - toolchain: ["stable", "beta", "nightly"] + toolchain: ["stable", "beta"] continue-on-error: ${{ matrix.toolchain != 'stable' }} steps: @@ -178,6 +181,59 @@ jobs: name: webserver-${{ matrix.toolchain }} path: web_ext/sseq_gui/dist/ + # Build the wasm webserver that actually gets deployed, on nightly with + # `panic=unwind` (via WASM_UNWIND=1). This is deploy-critical, so unlike the + # other nightly jobs it is not allowed to fail. + webserver-deploy: + if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository }} + runs-on: ubuntu-latest + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-D warnings" + RUSTUP_TOOLCHAIN: nightly + + steps: + - uses: actions/checkout@v4 + + - name: Install rustup + uses: dtolnay/rust-toolchain@v1 + id: rustup + with: + toolchain: nightly + targets: wasm32-unknown-unknown + # rust-src is required by `-Z build-std` (used by WASM_UNWIND=1). + components: clippy, rustfmt, rust-src + + - name: Cache files + uses: actions/cache@v4 + with: + path: | + ~/.cargo + /usr/share/rust/.cargo + **/target + key: wasm-unwind-${{ steps.rustup.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.*') }} + + - name: Install wasm-opt and wasm-objdump + run: sudo apt-get install -y binaryen wabt + + - name: Setup build environment + run: make -C web_ext/sseq_gui setup-wasm + + - name: Build wasm (panic=unwind) + run: make -C web_ext/sseq_gui wasm WASM_UNWIND=1 + + - name: Verify wasm unwinding + run: make -C web_ext/sseq_gui test-wasm-unwind + + - name: Benchmark wasm size + run: ls -l web_ext/sseq_gui/dist/sseq_gui_wasm_bg.wasm + + - name: Upload webserver + uses: actions/upload-artifact@v4 + with: + name: webserver-nightly + path: web_ext/sseq_gui/dist/ + selenium: if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository }} runs-on: ubuntu-latest @@ -355,15 +411,16 @@ jobs: path: ext/target/doc/ deploy: - needs: [test, lint, webserver, calculator, docs, selenium] + needs: [test, lint, webserver, webserver-deploy, calculator, docs, selenium] runs-on: ubuntu-latest if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} steps: - name: Download webserver + # Deploy the nightly build, which is compiled with panic=unwind. uses: actions/download-artifact@v4 with: - name: webserver-stable + name: webserver-nightly - name: Download calculator uses: actions/download-artifact@v4 diff --git a/web_ext/sseq_gui/Makefile b/web_ext/sseq_gui/Makefile index f689266559..378564729f 100644 --- a/web_ext/sseq_gui/Makefile +++ b/web_ext/sseq_gui/Makefile @@ -5,14 +5,20 @@ WASM_OUT = dist/ WASM_FILE = $(WASM_OUT)/$(NAME)_wasm_bg.wasm EXT = ../../ext/ -EXT_SRC = $(EXT)/Cargo.toml $(shell find $(EXT)/src/) $(wildcard $(EXT)/crates/*/Cargo.tml) $(shell find $(EXT)/crates/*/src/) - -# The prebuilt std for wasm32-unknown-unknown is compiled with `panic=abort`, -# so `-C panic=unwind` alone is silently ignored. To get real unwinding we must -# rebuild std with `-Z build-std` (nightly only) and enable the wasm -# exception-handling proposal. +# Building with `panic=unwind` lets Rust panics unwind into JS exceptions +# instead of aborting the whole wasm module. The prebuilt std for +# wasm32-unknown-unknown is compiled with `panic=abort`, so `-C panic=unwind` +# alone is silently ignored: we must rebuild std with `-Z build-std` (nightly +# only) and enable the wasm exception-handling proposal. +# +# This is opt-in via `WASM_UNWIND=1` so the default build still works on +# stable/beta (panic=abort). CI builds stable/beta with panic=abort and the +# deployed build with `WASM_UNWIND=1` on nightly. +ifdef WASM_UNWIND WASM_RUSTFLAGS = -C panic=unwind -C target-feature=+exception-handling WASM_BUILD_STD = -Z build-std=std,panic_unwind +WASM_OPT_FLAGS = --enable-exception-handling +endif export PATH := $(HOME)/.cargo/bin:$(PATH) @@ -30,23 +36,32 @@ setup-wasm: # rust-src is required by `-Z build-std` (see the WASM_BUILD_STD note above). rustup component add rust-src -$(WASM_LIB): Cargo.toml $(wildcard src/*) $(EXT_SRC) +# Always defer to cargo to decide whether a rebuild is needed: it fingerprints +# RUSTFLAGS and the build-std setting, so it correctly rebuilds when toggling +# between panic=abort and panic=unwind (which `make`'s timestamp check cannot +# detect, as the sources are unchanged). +$(WASM_LIB): FORCE RUSTFLAGS="$$RUSTFLAGS $(WASM_RUSTFLAGS)" \ cargo build --lib --target $(WASM_TARGET) --release $(WASM_BUILD_STD) +FORCE: + $(WASM_FILE): $(WASM_LIB) wasm-bindgen --no-typescript --target no-modules --out-dir $(WASM_OUT) --out-name $(NAME)_wasm $(WASM_LIB) - wasm-opt -O3 --enable-exception-handling $(WASM_FILE) -o $(WASM_FILE) || true + wasm-opt -O3 $(WASM_OPT_FLAGS) $(WASM_FILE) -o $(WASM_FILE) || true # Assert that the wasm is actually built with unwinding support: a wasm module # using the exception-handling proposal contains a `Tag` section. If unwinding # silently regressed to `panic=abort` (e.g. build-std got dropped), this fails. -test-wasm-unwind: $(WASM_LIB) +# Forces a WASM_UNWIND=1 build of the lib so the check is meaningful regardless +# of how it was invoked. +test-wasm-unwind: + $(MAKE) WASM_UNWIND=1 $(WASM_LIB) wasm-objdump -h $(WASM_LIB) | grep -qi '^ *Tag ' \ || { echo "ERROR: $(WASM_LIB) has no Tag section; panic=unwind is not in effect"; exit 1; } @echo "OK: wasm built with exception-handling (panic=unwind)" -.PHONY: wasm serve-wasm clean-wasm clean dummy test selenium selenium-update test-wasm-unwind +.PHONY: wasm serve-wasm clean-wasm clean dummy test selenium selenium-update test-wasm-unwind FORCE wasm: $(WASM_FILE) $(wildcard interface/*) $(wildcard wasm/*) $(wildcard $(EXT)/steenrod_modules/*) # Must be done in this order since both contain index.js and we want the wasm version diff --git a/web_ext/sseq_gui/README.md b/web_ext/sseq_gui/README.md index b6a2c1f503..5fbcbaa9f6 100644 --- a/web_ext/sseq_gui/README.md +++ b/web_ext/sseq_gui/README.md @@ -35,10 +35,18 @@ To setup the build environment, run make setup-wasm ``` -The wasm build rebuilds the standard library with `panic=unwind` (via -`-Z build-std`) so that Rust panics unwind into JavaScript exceptions instead of -aborting the whole module. This requires a **nightly** toolchain with the -`rust-src` component (installed by `make setup-wasm`). +By default the wasm is built with `panic=abort`, which works on stable. Passing +`WASM_UNWIND=1` instead rebuilds the standard library with `panic=unwind` (via +`-Z build-std`) so that Rust panics unwind into JavaScript exceptions rather +than aborting the whole module: + +```shell +make wasm WASM_UNWIND=1 +``` + +This requires a **nightly** toolchain with the `rust-src` component (installed +by `make setup-wasm`). The deployed build uses `WASM_UNWIND=1`; CI also builds +the default `panic=abort` configuration on stable/beta. Afterwards, build and serve with