From c6714cd20c5995935f41c74d2287bc845401978c Mon Sep 17 00:00:00 2001 From: Christopher Hoover Date: Mon, 28 Sep 2026 16:09:37 -0700 Subject: [PATCH 1/2] fix(wasm): enable C++ exception catching in the Emscripten build Emscripten disables exception catching by default, so throws abort and catch blocks are elided. nec_context uses `throw 1` to unwind a finished solve, and nec_process_input relies on catching nec_exception to report errors. Pass -fexceptions at both compile and link time. Co-Authored-By: Claude Opus 5.5 --- src/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7af746d7..a3044d00 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -149,9 +149,15 @@ if(NECPP_BUILD_WASM) ${CMAKE_CURRENT_SOURCE_DIR} ${NECPP_EIGEN_DIR} ${CMAKE_BINARY_DIR}) + # Emscripten disables exception catching by default. nec_context throws to + # unwind a finished solve, so without this the catch is elided and every + # completed run escapes as an uncaught error. Needed at compile, not just + # link: the link flag cannot restore a catch already dropped. + target_compile_options(nec2pp_wasm PRIVATE -fexceptions) # Flags carried verbatim from the old Makefile wasm target. target_link_options(nec2pp_wasm PRIVATE -sWASM=1 + -fexceptions "-sEXPORTED_FUNCTIONS=[\"_nec_create_context\",\"_nec_delete_context\",\"_nec_process_input\",\"_nec_get_output\",\"_nec_get_output_length\",\"_nec_free\"]" "-sEXPORTED_RUNTIME_METHODS=[\"ccall\",\"cwrap\",\"UTF8ToString\",\"lengthBytesUTF8\"]" -sALLOW_MEMORY_GROWTH=1 From 5b74cdcba8b302272ae732a5fc0f4b182bbbf3d1 Mon Sep 17 00:00:00 2001 From: Christopher Hoover Date: Mon, 28 Sep 2026 16:23:34 -0700 Subject: [PATCH 2/2] test(wasm): add a Node runtime smoke test for the Emscripten build The CI wasm job only checked that nec2pp.js/nec2pp.wasm were produced. Load the module under Node, feed nec_process_input a deck that makes the geometry parser throw, and require rc -2 with the error in the output. Without exception catching the throw escapes into JS and the test fails. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 3 +++ tests/wasm_smoke.js | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/wasm_smoke.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31b4c38b..c08dfa56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,3 +70,6 @@ jobs: test -f nec2pp.js test -f nec2pp.wasm echo "WASM artifacts present: $(ls -la nec2pp.js nec2pp.wasm)" + + - name: WASM runtime smoke test + run: node tests/wasm_smoke.js nec2pp.js diff --git a/tests/wasm_smoke.js b/tests/wasm_smoke.js new file mode 100644 index 00000000..11ddb68b --- /dev/null +++ b/tests/wasm_smoke.js @@ -0,0 +1,48 @@ +// Runtime smoke test for the Emscripten build (nec2pp.js + nec2pp.wasm). +// +// Usage: node tests/wasm_smoke.js [path/to/nec2pp.js] +// +// Feeds nec_process_input a deck that makes the geometry parser throw a +// nec_exception and checks it comes back as rc -2 with the message in the +// output buffer. If the build lacks C++ exception catching, the throw +// escapes into JS instead and this test fails. +// +// nec_process_input is still a stub that parses geometry from stdin rather +// than from its input string, so stdin is set to EOF here. +'use strict'; + +const path = require('path'); + +const jsPath = path.resolve(process.argv[2] || 'nec2pp.js'); +const M = require(jsPath); + +function fail(msg) { + console.error('FAIL: ' + msg); + process.exit(1); +} + +M.stdin = () => null; +M.onAbort = (what) => fail('module aborted: ' + what); +M.onRuntimeInitialized = () => { + const ctx = M.ccall('nec_create_context', 'number', [], []); + if (!ctx) + fail('nec_create_context returned null'); + + let rc; + try { + rc = M.ccall('nec_process_input', 'number', ['number', 'string'], + [ctx, 'CE\nEN\n']); + } catch (e) { + fail('nec_process_input threw into JS (exception catching disabled?): ' + e); + } + + const out = M.UTF8ToString(M.ccall('nec_get_output', 'number', ['number'], [ctx])); + M.ccall('nec_delete_context', null, ['number'], [ctx]); + + if (rc !== -2) + fail('expected rc -2, got ' + rc + ' (output: ' + JSON.stringify(out) + ')'); + if (!out.startsWith('Error: GEOMETRY DATA CARD ERROR')) + fail('unexpected output: ' + JSON.stringify(out)); + + console.log('WASM smoke test passed: ' + out); +};