From 5fddb4bb9192edfedd20dc8046a175cf69cf7901 Mon Sep 17 00:00:00 2001 From: leftibot Date: Sat, 2 May 2026 12:36:16 -0600 Subject: [PATCH 1/3] Fix #155: switch WASM builds from legacy Asyncify to JSPI cmake/Emscripten.cmake set both -fwasm-exceptions (compile + link) and -sASYNCIFY=1 (per-target link), and recent emsdk releases hard-fail the combination in wasm-opt with `__asyncify_get_call_index does not exist`, breaking the wasm.yml workflow. JSPI (JavaScript Promise Integration) is the upstream-recommended replacement for legacy Asyncify and is compatible with native WebAssembly exception handling, so the per-target flag now reads -sJSPI=1 and the no-longer-relevant myproject_WASM_ASYNCIFY_STACK_SIZE cache variable is dropped. A new CMake-script regression test stubs the target/global option commands, includes Emscripten.cmake under EMSCRIPTEN=TRUE, and asserts that the legacy flags are gone, JSPI is emitted, and -fwasm-exceptions remains. --- cmake/Emscripten.cmake | 10 +- test/CMakeLists.txt | 9 ++ test/cmake/test_emscripten_modern_async.cmake | 128 ++++++++++++++++++ 3 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 test/cmake/test_emscripten_modern_async.cmake diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake index cc59389b..42c90b1b 100644 --- a/cmake/Emscripten.cmake +++ b/cmake/Emscripten.cmake @@ -42,8 +42,6 @@ if(EMSCRIPTEN) "Initial WASM memory in bytes (default: 32MB)") set(myproject_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING "Pthread pool size for WASM builds (default: 4)") - set(myproject_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING - "Asyncify stack size in bytes (default: 64KB)") # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling # Set these flags early so they propagate to all dependencies @@ -89,9 +87,11 @@ function(myproject_configure_wasm_target target) "-sUSE_PTHREADS=1" "-sPROXY_TO_PTHREAD=1" "-sPTHREAD_POOL_SIZE=${myproject_WASM_PTHREAD_POOL_SIZE}" - # Enable asyncify for emscripten_sleep and async operations - "-sASYNCIFY=1" - "-sASYNCIFY_STACK_SIZE=${myproject_WASM_ASYNCIFY_STACK_SIZE}" + # Enable JSPI (JavaScript Promise Integration) for emscripten_sleep + # and other async operations. JSPI is the modern replacement for + # legacy Asyncify (-sASYNCIFY=1) and, unlike Asyncify, is compatible + # with native WebAssembly exception handling (-fwasm-exceptions). + "-sJSPI=1" # Memory configuration "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=${myproject_WASM_INITIAL_MEMORY}" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bea3498f..df6f6fd2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,6 +34,15 @@ add_test( COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/test_sanitizers_non_apple_includes_leak.cmake) +# CMake-script regression test for cmake/Emscripten.cmake (issue #155): +# the legacy `-sASYNCIFY=1` flag is incompatible with `-fwasm-exceptions` +# under modern emsdk; the WASM target must use the JSPI replacement. +add_test( + NAME cmake.emscripten.modern_async + COMMAND ${CMAKE_COMMAND} + -DTEST_TMP_DIR=${CMAKE_CURRENT_BINARY_DIR}/test_emscripten_modern_async + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/test_emscripten_modern_async.cmake) + # Provide a test to verify that the version being reported from the application # matches the version given to CMake. This will be important once you package # your program. Real world shows that this is the kind of simple mistake that is easy diff --git a/test/cmake/test_emscripten_modern_async.cmake b/test/cmake/test_emscripten_modern_async.cmake new file mode 100644 index 00000000..9e653ab7 --- /dev/null +++ b/test/cmake/test_emscripten_modern_async.cmake @@ -0,0 +1,128 @@ +#[[ +Regression test for #155: WASM builds were broken because +cmake/Emscripten.cmake combined two flags that newer Emscripten releases +reject in the same translation unit: + + * -fwasm-exceptions (set via add_compile_options / add_link_options) + * -sASYNCIFY=1 (set via target_link_options on each WASM target) + +The link step ended in `wasm-opt` with +`Fatal: Module::getFunction: __asyncify_get_call_index does not exist` +because the legacy Asyncify transformation is incompatible with native +WebAssembly exception handling. + +Per the issue ("Prefer the modern replacement features") the project +should use JSPI (JavaScript Promise Integration) instead, which is the +upstream-recommended replacement for legacy Asyncify and works alongside +-fwasm-exceptions. + +This test simulates an EMSCRIPTEN configuration, stubs the CMake commands +that touch real targets, includes Emscripten.cmake, and asserts that: + + 1. The legacy `-sASYNCIFY=1` flag is NOT emitted (regression guard). + 2. The legacy `-sASYNCIFY_STACK_SIZE` flag is NOT emitted (irrelevant + for JSPI). + 3. The modern `-sJSPI=1` flag IS emitted on the WASM target. + 4. `-fwasm-exceptions` is still active on the global compile/link + options so native EH continues to be used. +]] + +cmake_minimum_required(VERSION 3.21) + +set(EMSCRIPTEN TRUE) + +# Use the real source tree so EXISTS checks against the shell template pass +# and Emscripten.cmake's input templates resolve correctly. +set(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") + +# Caller (CTest) supplies an out-of-tree scratch dir via -DTEST_TMP_DIR=...; +# fall back to the platform temp dir when the script is invoked manually. +if(NOT DEFINED TEST_TMP_DIR OR TEST_TMP_DIR STREQUAL "") + set(TEST_TMP_DIR "$ENV{TMPDIR}") + if(TEST_TMP_DIR STREQUAL "") + set(TEST_TMP_DIR "/tmp") + endif() + set(TEST_TMP_DIR "${TEST_TMP_DIR}/cmake_template_test_emscripten_modern_async") +endif() +set(CMAKE_BINARY_DIR "${TEST_TMP_DIR}") +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}") + +set(CAPTURE_FILE "${CMAKE_BINARY_DIR}/captured_options.txt") +file(WRITE "${CAPTURE_FILE}" "") + +# Capture global compile / link options emitted at module-include time. +function(add_compile_options) + file(APPEND "${CAPTURE_FILE}" "ADD_COMPILE_OPTIONS:${ARGN}\n") +endfunction() + +function(add_link_options) + file(APPEND "${CAPTURE_FILE}" "ADD_LINK_OPTIONS:${ARGN}\n") +endfunction() + +# Capture per-target link options. Drop the leading args +# so the captured payload is easy to grep. +function(target_link_options) + set(_args "${ARGN}") + list(REMOVE_AT _args 0 1) + file(APPEND "${CAPTURE_FILE}" "TARGET_LINK_OPTIONS:${_args}\n") +endfunction() + +# Stub out the rest of the target-affecting commands so script mode +# doesn't try to look up a real CMake target. +function(target_compile_definitions) +endfunction() + +function(get_target_property var target prop) + set(${var} "${target}" PARENT_SCOPE) +endfunction() + +function(set_target_properties) +endfunction() + +function(add_custom_command) +endfunction() + +function(configure_file) +endfunction() + +function(set_property) +endfunction() + +include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/Emscripten.cmake") + +myproject_configure_wasm_target( + fake_wasm_target + TITLE "Fake" + DESCRIPTION "Fake WASM target for regression test #155") + +file(READ "${CAPTURE_FILE}" captures) +message(STATUS "Captured Emscripten options:\n${captures}") + +if(captures MATCHES "ASYNCIFY=1") + message( + FATAL_ERROR + "Legacy -sASYNCIFY=1 must not be emitted: it is incompatible with " + "-fwasm-exceptions and breaks WASM linking (#155). Captured:\n${captures}") +endif() + +if(captures MATCHES "ASYNCIFY_STACK_SIZE") + message( + FATAL_ERROR + "-sASYNCIFY_STACK_SIZE is only meaningful for legacy Asyncify and " + "should be removed alongside -sASYNCIFY=1 (#155). Captured:\n${captures}") +endif() + +if(NOT captures MATCHES "-sJSPI=1") + message( + FATAL_ERROR + "Expected modern -sJSPI=1 flag (replacement for legacy Asyncify) on " + "the WASM target (#155). Captured:\n${captures}") +endif() + +if(NOT captures MATCHES "-fwasm-exceptions") + message( + FATAL_ERROR + "-fwasm-exceptions must remain enabled for WASM builds; the fix " + "for #155 should switch async support, not disable native EH. " + "Captured:\n${captures}") +endif() From 252105569dc7564774594bd831f1206d237d01b2 Mon Sep 17 00:00:00 2001 From: leftibot Date: Sat, 2 May 2026 14:30:08 -0600 Subject: [PATCH 2/3] Address review: remove WASM cmake-script regression test Requested by @lefticus in PR #156 review. --- test/CMakeLists.txt | 9 -- test/cmake/test_emscripten_modern_async.cmake | 128 ------------------ 2 files changed, 137 deletions(-) delete mode 100644 test/cmake/test_emscripten_modern_async.cmake diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index df6f6fd2..bea3498f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,15 +34,6 @@ add_test( COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/test_sanitizers_non_apple_includes_leak.cmake) -# CMake-script regression test for cmake/Emscripten.cmake (issue #155): -# the legacy `-sASYNCIFY=1` flag is incompatible with `-fwasm-exceptions` -# under modern emsdk; the WASM target must use the JSPI replacement. -add_test( - NAME cmake.emscripten.modern_async - COMMAND ${CMAKE_COMMAND} - -DTEST_TMP_DIR=${CMAKE_CURRENT_BINARY_DIR}/test_emscripten_modern_async - -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/test_emscripten_modern_async.cmake) - # Provide a test to verify that the version being reported from the application # matches the version given to CMake. This will be important once you package # your program. Real world shows that this is the kind of simple mistake that is easy diff --git a/test/cmake/test_emscripten_modern_async.cmake b/test/cmake/test_emscripten_modern_async.cmake deleted file mode 100644 index 9e653ab7..00000000 --- a/test/cmake/test_emscripten_modern_async.cmake +++ /dev/null @@ -1,128 +0,0 @@ -#[[ -Regression test for #155: WASM builds were broken because -cmake/Emscripten.cmake combined two flags that newer Emscripten releases -reject in the same translation unit: - - * -fwasm-exceptions (set via add_compile_options / add_link_options) - * -sASYNCIFY=1 (set via target_link_options on each WASM target) - -The link step ended in `wasm-opt` with -`Fatal: Module::getFunction: __asyncify_get_call_index does not exist` -because the legacy Asyncify transformation is incompatible with native -WebAssembly exception handling. - -Per the issue ("Prefer the modern replacement features") the project -should use JSPI (JavaScript Promise Integration) instead, which is the -upstream-recommended replacement for legacy Asyncify and works alongside --fwasm-exceptions. - -This test simulates an EMSCRIPTEN configuration, stubs the CMake commands -that touch real targets, includes Emscripten.cmake, and asserts that: - - 1. The legacy `-sASYNCIFY=1` flag is NOT emitted (regression guard). - 2. The legacy `-sASYNCIFY_STACK_SIZE` flag is NOT emitted (irrelevant - for JSPI). - 3. The modern `-sJSPI=1` flag IS emitted on the WASM target. - 4. `-fwasm-exceptions` is still active on the global compile/link - options so native EH continues to be used. -]] - -cmake_minimum_required(VERSION 3.21) - -set(EMSCRIPTEN TRUE) - -# Use the real source tree so EXISTS checks against the shell template pass -# and Emscripten.cmake's input templates resolve correctly. -set(CMAKE_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") - -# Caller (CTest) supplies an out-of-tree scratch dir via -DTEST_TMP_DIR=...; -# fall back to the platform temp dir when the script is invoked manually. -if(NOT DEFINED TEST_TMP_DIR OR TEST_TMP_DIR STREQUAL "") - set(TEST_TMP_DIR "$ENV{TMPDIR}") - if(TEST_TMP_DIR STREQUAL "") - set(TEST_TMP_DIR "/tmp") - endif() - set(TEST_TMP_DIR "${TEST_TMP_DIR}/cmake_template_test_emscripten_modern_async") -endif() -set(CMAKE_BINARY_DIR "${TEST_TMP_DIR}") -file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}") - -set(CAPTURE_FILE "${CMAKE_BINARY_DIR}/captured_options.txt") -file(WRITE "${CAPTURE_FILE}" "") - -# Capture global compile / link options emitted at module-include time. -function(add_compile_options) - file(APPEND "${CAPTURE_FILE}" "ADD_COMPILE_OPTIONS:${ARGN}\n") -endfunction() - -function(add_link_options) - file(APPEND "${CAPTURE_FILE}" "ADD_LINK_OPTIONS:${ARGN}\n") -endfunction() - -# Capture per-target link options. Drop the leading args -# so the captured payload is easy to grep. -function(target_link_options) - set(_args "${ARGN}") - list(REMOVE_AT _args 0 1) - file(APPEND "${CAPTURE_FILE}" "TARGET_LINK_OPTIONS:${_args}\n") -endfunction() - -# Stub out the rest of the target-affecting commands so script mode -# doesn't try to look up a real CMake target. -function(target_compile_definitions) -endfunction() - -function(get_target_property var target prop) - set(${var} "${target}" PARENT_SCOPE) -endfunction() - -function(set_target_properties) -endfunction() - -function(add_custom_command) -endfunction() - -function(configure_file) -endfunction() - -function(set_property) -endfunction() - -include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/Emscripten.cmake") - -myproject_configure_wasm_target( - fake_wasm_target - TITLE "Fake" - DESCRIPTION "Fake WASM target for regression test #155") - -file(READ "${CAPTURE_FILE}" captures) -message(STATUS "Captured Emscripten options:\n${captures}") - -if(captures MATCHES "ASYNCIFY=1") - message( - FATAL_ERROR - "Legacy -sASYNCIFY=1 must not be emitted: it is incompatible with " - "-fwasm-exceptions and breaks WASM linking (#155). Captured:\n${captures}") -endif() - -if(captures MATCHES "ASYNCIFY_STACK_SIZE") - message( - FATAL_ERROR - "-sASYNCIFY_STACK_SIZE is only meaningful for legacy Asyncify and " - "should be removed alongside -sASYNCIFY=1 (#155). Captured:\n${captures}") -endif() - -if(NOT captures MATCHES "-sJSPI=1") - message( - FATAL_ERROR - "Expected modern -sJSPI=1 flag (replacement for legacy Asyncify) on " - "the WASM target (#155). Captured:\n${captures}") -endif() - -if(NOT captures MATCHES "-fwasm-exceptions") - message( - FATAL_ERROR - "-fwasm-exceptions must remain enabled for WASM builds; the fix " - "for #155 should switch async support, not disable native EH. " - "Captured:\n${captures}") -endif() From cd295c00b512cbed3f3ff2d8ddded536e5959094 Mon Sep 17 00:00:00 2001 From: leftibot Date: Sat, 2 May 2026 14:55:29 -0600 Subject: [PATCH 3/3] Address review: switch WASM EH to -fexceptions, restore Asyncify Native wasm exceptions (-fwasm-exceptions) are incompatible with legacy Asyncify, and JSPI is not yet available in all browsers. Switch the Emscripten exception model to -fexceptions (JS-based) and restore -sASYNCIFY=1 / -sASYNCIFY_STACK_SIZE so the WASM build keeps working across every browser that supports threads, until JSPI is universally available. Requested by @lefticus in PR #156 review. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmake/Emscripten.cmake | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake index 42c90b1b..258c89b0 100644 --- a/cmake/Emscripten.cmake +++ b/cmake/Emscripten.cmake @@ -42,11 +42,16 @@ if(EMSCRIPTEN) "Initial WASM memory in bytes (default: 32MB)") set(myproject_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING "Pthread pool size for WASM builds (default: 4)") - - # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling - # Set these flags early so they propagate to all dependencies - add_compile_options(-pthread -fwasm-exceptions) - add_link_options(-pthread -fwasm-exceptions) + set(myproject_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING + "Asyncify stack size in bytes (default: 64KB)") + + # For Emscripten WASM builds, FTXUI requires pthreads and exception handling. + # We use -fexceptions (JS-based exceptions) rather than -fwasm-exceptions + # because native wasm exceptions are incompatible with legacy Asyncify, and + # JSPI (the modern Asyncify replacement) is not yet shipping in all browsers. + # Set these flags early so they propagate to all dependencies. + add_compile_options(-pthread -fexceptions) + add_link_options(-pthread -fexceptions) endif() # Function to apply WASM settings to a target @@ -87,11 +92,11 @@ function(myproject_configure_wasm_target target) "-sUSE_PTHREADS=1" "-sPROXY_TO_PTHREAD=1" "-sPTHREAD_POOL_SIZE=${myproject_WASM_PTHREAD_POOL_SIZE}" - # Enable JSPI (JavaScript Promise Integration) for emscripten_sleep - # and other async operations. JSPI is the modern replacement for - # legacy Asyncify (-sASYNCIFY=1) and, unlike Asyncify, is compatible - # with native WebAssembly exception handling (-fwasm-exceptions). - "-sJSPI=1" + # Enable Asyncify for emscripten_sleep and other async operations. + # Paired with -fexceptions (JS-based exception handling) above, since + # Asyncify is not compatible with native wasm exceptions. + "-sASYNCIFY=1" + "-sASYNCIFY_STACK_SIZE=${myproject_WASM_ASYNCIFY_STACK_SIZE}" # Memory configuration "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=${myproject_WASM_INITIAL_MEMORY}"