fix(wasm): Fix asyncify rewind with asyncify-aware dynCall shims

Emscripten 4.x removed dynCall_* WASM exports, breaking asyncify
rewind through indirect calls (modal dialogs, event handlers).
Generate JS shims that track Asyncify.exportCallStack and register
in wasmExports so doRewind can find them.

Also fixes empty callback functions ((() => {})) generated by
Emscripten 4.x + pthreads for HTML5 events, pthread entry,
sighandler, async timer, and main loop callbacks.

Build pipeline improvements:
- Stub wasm-opt/finalize in Docker (RAM limits), run on host
- Add setup-emsdk.sh for reproducible Emscripten setup
- Simplify env.sh and version management

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2026-03-13 12:26:48 +01:00
commit b46b4b69f1
13 changed files with 150 additions and 65 deletions

View file

@ -36,47 +36,18 @@ export WX_BUILD="$BUILD_ROOT/wxwidgets-universal"
# Emscripten settings
export EMSDK_QUIET=1
# Homebrew Emscripten configuration
# The em++ script uses EMSDK_PYTHON (not PYTHON env var) for the Python interpreter
# and finds clang via PATH - Emscripten bundles its own LLVM with WebAssembly support
if [[ -d "/opt/homebrew/Cellar/emscripten" ]]; then
_EM_VERSION=$(ls /opt/homebrew/Cellar/emscripten/ | sort -V | tail -1)
_EM_LLVM_BIN="/opt/homebrew/Cellar/emscripten/$_EM_VERSION/libexec/llvm/bin"
if [[ -d "$_EM_LLVM_BIN" ]]; then
# Add bundled LLVM to PATH so Emscripten finds its clang (not /usr/bin/clang)
export PATH="$_EM_LLVM_BIN:$PATH"
fi
elif [[ -d "/usr/local/Cellar/emscripten" ]]; then
_EM_VERSION=$(ls /usr/local/Cellar/emscripten/ | sort -V | tail -1)
_EM_LLVM_BIN="/usr/local/Cellar/emscripten/$_EM_VERSION/libexec/llvm/bin"
if [[ -d "$_EM_LLVM_BIN" ]]; then
export PATH="$_EM_LLVM_BIN:$PATH"
fi
# Local emsdk: auto-install if missing, then source its environment
# The emsdk bundles its own Python, Node, and LLVM — no Homebrew needed
_EMSDK_DIR="$_KICAD_WASM_PROJECT_ROOT/tools/emsdk"
_EMSDK_ENV="$_EMSDK_DIR/emsdk_env.sh"
if [ ! -f "$_EMSDK_ENV" ]; then
echo "Emscripten SDK not found. Installing..."
"$_KICAD_WASM_SCRIPTS_DIR/setup-emsdk.sh"
fi
# Emscripten 4.0.22+ requires Python 3.10+ (uses match statement and type union syntax)
# The em++ shell script checks EMSDK_PYTHON first, then falls back to `which python3`
# Set EMSDK_PYTHON to Homebrew's Python to ensure correct version is used
if [[ -d "/opt/homebrew/opt/python@3.14/bin" ]]; then
export EMSDK_PYTHON="/opt/homebrew/opt/python@3.14/bin/python3.14"
elif [[ -d "/opt/homebrew/opt/python@3.13/bin" ]]; then
export EMSDK_PYTHON="/opt/homebrew/opt/python@3.13/bin/python3.13"
elif [[ -d "/opt/homebrew/opt/python@3.12/bin" ]]; then
export EMSDK_PYTHON="/opt/homebrew/opt/python@3.12/bin/python3.12"
elif [[ -d "/opt/homebrew/opt/python@3.11/bin" ]]; then
export EMSDK_PYTHON="/opt/homebrew/opt/python@3.11/bin/python3.11"
elif [[ -d "/opt/homebrew/opt/python@3.10/bin" ]]; then
export EMSDK_PYTHON="/opt/homebrew/opt/python@3.10/bin/python3.10"
elif [[ -d "/usr/local/opt/python@3.14/bin" ]]; then
export EMSDK_PYTHON="/usr/local/opt/python@3.14/bin/python3.14"
elif [[ -d "/usr/local/opt/python@3.13/bin" ]]; then
export EMSDK_PYTHON="/usr/local/opt/python@3.13/bin/python3.13"
elif [[ -d "/usr/local/opt/python@3.12/bin" ]]; then
export EMSDK_PYTHON="/usr/local/opt/python@3.12/bin/python3.12"
elif [[ -d "/usr/local/opt/python@3.11/bin" ]]; then
export EMSDK_PYTHON="/usr/local/opt/python@3.11/bin/python3.11"
elif [[ -d "/usr/local/opt/python@3.10/bin" ]]; then
export EMSDK_PYTHON="/usr/local/opt/python@3.10/bin/python3.10"
if [ -f "$_EMSDK_ENV" ]; then
source "$_EMSDK_ENV" 2>/dev/null
fi
# Common compiler flags

View file

@ -53,7 +53,7 @@ setup_error_trap() {
# Verify Emscripten is available
verify_emscripten() {
if ! command -v emcc &> /dev/null; then
log_error "Emscripten not found. Please run: source /path/to/emsdk/emsdk_env.sh"
log_error "Emscripten not found. Run: ./scripts/setup-emsdk.sh"
exit 1
fi
log_info "Using Emscripten: $(emcc --version | head -1)"

View file

@ -1,17 +1,35 @@
#!/bin/bash
# Download and cache Binaryen wasm-opt
# Get path to Binaryen wasm-opt
#
# Usage: ./scripts/tools/get-wasm-opt.sh
# Usage: ./scripts/common/get-wasm-opt.sh
# Output: Prints path to wasm-opt executable
#
# Binaryen v121 is used because v125 has a regression causing asyncify crashes.
# The binary is cached in tools/binaryen-121/
# Prefers the emsdk-bundled Binaryen (tools/emsdk/upstream/bin/) so that the
# wasm-opt and wasm-emscripten-finalize versions match the Emscripten that
# generated the JS glue. A version mismatch between the compiler's Binaryen
# (e.g. v121+72 dev) and a standalone release (v121) corrupts asyncify
# metadata, causing "func is not a function" errors at runtime.
#
# Falls back to downloading standalone Binaryen v121 if emsdk is not installed
# locally (e.g. CI environments that only use Docker).
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# --- Prefer emsdk-bundled Binaryen (matches the Emscripten that compiled the WASM) ---
EMSDK_WASM_OPT="${PROJECT_ROOT}/tools/emsdk/upstream/bin/wasm-opt"
if [ -x "${EMSDK_WASM_OPT}" ]; then
EMSDK_VERSION=$("${EMSDK_WASM_OPT}" --version 2>&1 || true)
echo "Using emsdk-bundled Binaryen: ${EMSDK_VERSION}" >&2
echo "${EMSDK_WASM_OPT}"
exit 0
fi
# --- Fallback: download standalone Binaryen (for CI or environments without local emsdk) ---
echo "emsdk Binaryen not found at ${EMSDK_WASM_OPT}, falling back to standalone download..." >&2
BINARYEN_VERSION="121"
BINARYEN_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}"
WASM_OPT="${BINARYEN_DIR}/bin/wasm-opt"

View file

@ -38,6 +38,11 @@ cat > "$SHIM_FILE" << 'HEADER'
// === dynCall shims for Emscripten exception handling ===
// Auto-generated: maps dynCall_SIG() calls to getWasmTableEntry()
// This fixes "dynCall_* is not defined" errors in Emscripten 4.x
//
// These shims are asyncify-aware: they track Asyncify.exportCallStack so that
// asyncify can rewind through indirect calls (e.g., main loop callbacks,
// timer callbacks, event handlers). Without this tracking, asyncify's doRewind
// fails because it can't find the entry function to re-enter the WASM module.
HEADER
for sig in $SIGNATURES; do
@ -55,12 +60,31 @@ for sig in $SIGNATURES; do
call_args="${call_args}a$i"
done
echo "var dynCall_$sig = ($args) => getWasmTableEntry(index)($call_args);" >> "$SHIM_FILE"
cat >> "$SHIM_FILE" << SHIMEOF
function dynCall_$sig($args) {
var tableFunc = getWasmTableEntry(index);
if (typeof Asyncify !== 'undefined') {
var rewindKey = '__dyn_${sig}_' + index;
if (!wasmExports[rewindKey]) wasmExports[rewindKey] = tableFunc;
Asyncify.exportCallStack.push(rewindKey);
try {
return tableFunc($call_args);
} finally {
if (!ABORT) {
Asyncify.exportCallStack.pop();
Asyncify.maybeStopUnwind();
}
}
}
return tableFunc($call_args);
}
SHIMEOF
done
echo "" >> "$SHIM_FILE"
echo "// === End dynCall shims ===" >> "$SHIM_FILE"
# Find the insertion point: after getWasmTableEntry definition
# The pattern is:
# var getWasmTableEntry = funcPtr => {
@ -169,6 +193,18 @@ if [ "$COUNT_BEFORE" -gt 0 ]; then
TOTAL_FIXED=$((TOTAL_FIXED + FIXED))
fi
# Fix 5: _emscripten_set_main_loop empty iterFunc callback - signature v (void, no args)
# Pattern: var iterFunc = (() => {});
# The 'func' variable is the function pointer passed to _emscripten_set_main_loop
COUNT_BEFORE=$(grep -c 'var iterFunc = (() => {});' "$JS_FILE" || true)
if [ "$COUNT_BEFORE" -gt 0 ]; then
sed -i '' 's/var iterFunc = (() => {});/var iterFunc = () => dynCall_v(func);/g' "$JS_FILE"
COUNT_AFTER=$(grep -c 'var iterFunc = (() => {});' "$JS_FILE" || true)
FIXED=$((COUNT_BEFORE - COUNT_AFTER))
echo " Fixed $FIXED main loop callback(s) (dynCall_v)"
TOTAL_FIXED=$((TOTAL_FIXED + FIXED))
fi
if [ "$TOTAL_FIXED" -gt 0 ]; then
echo "Total: Fixed $TOTAL_FIXED empty callback(s)"
else

View file

@ -2,6 +2,9 @@
# Dependency versions for KiCad WASM build
# These versions match KiCad 8.99 requirements from CMakeLists.txt and vcpkg.json
# Emscripten SDK version (single source of truth for Docker and local builds)
export EMSCRIPTEN_VERSION="4.0.2"
# KiCad submodule version
export KICAD_COMMIT="4bfed3f1746e8cc0a7d942767770f56fa28b393c"
export KICAD_VERSION="8.99"