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>
212 lines
7.5 KiB
Shell
Executable file
212 lines
7.5 KiB
Shell
Executable file
#!/bin/bash
|
|
# Inject dynCall_* shims into Emscripten-generated JS
|
|
#
|
|
# Emscripten 4.x no longer generates signature-specific dynCall_* functions,
|
|
# but the invoke_* wrappers (for C++ exception handling) still call them.
|
|
# This script auto-generates shims using getWasmTableEntry() which IS defined.
|
|
#
|
|
# Usage: inject-dyncall-shims.sh <pcbnew.js>
|
|
|
|
set -e
|
|
|
|
JS_FILE="$1"
|
|
|
|
if [ -z "$JS_FILE" ] || [ ! -f "$JS_FILE" ]; then
|
|
echo "Error: JS file not found: $JS_FILE"
|
|
echo "Usage: $0 <path/to/pcbnew.js>"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Extracting dynCall signatures from $JS_FILE..."
|
|
|
|
# Extract all unique dynCall_* signatures from the file
|
|
# Matches patterns like: dynCall_i, dynCall_ii, dynCall_viijj, etc.
|
|
SIGNATURES=$(grep -oE 'dynCall_[a-zA-Z0-9]+' "$JS_FILE" | sort -u | sed 's/dynCall_//')
|
|
|
|
if [ -z "$SIGNATURES" ]; then
|
|
echo "No dynCall signatures found - nothing to inject"
|
|
exit 0
|
|
fi
|
|
|
|
SIG_COUNT=$(echo "$SIGNATURES" | wc -l | tr -d ' ')
|
|
echo "Found $SIG_COUNT unique signatures"
|
|
|
|
# Generate shim code
|
|
SHIM_FILE=$(mktemp)
|
|
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
|
|
# Count args: signature length - 1 (first char is return type)
|
|
argcount=$((${#sig} - 1))
|
|
|
|
# Generate argument list: index, a0, a1, a2, ...
|
|
args="index"
|
|
call_args=""
|
|
for ((i=0; i<argcount; i++)); do
|
|
args="$args, a$i"
|
|
if [ $i -gt 0 ]; then
|
|
call_args="$call_args, "
|
|
fi
|
|
call_args="${call_args}a$i"
|
|
done
|
|
|
|
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 => {
|
|
# ...
|
|
# };
|
|
# We insert after the closing `};`
|
|
|
|
# Find line number of getWasmTableEntry definition
|
|
GWTL_LINE=$(grep -n '^var getWasmTableEntry = funcPtr => {' "$JS_FILE" | head -1 | cut -d: -f1)
|
|
|
|
if [ -z "$GWTL_LINE" ]; then
|
|
echo "Warning: Could not find getWasmTableEntry definition"
|
|
echo "Trying alternate pattern..."
|
|
GWTL_LINE=$(grep -n 'var getWasmTableEntry' "$JS_FILE" | head -1 | cut -d: -f1)
|
|
fi
|
|
|
|
if [ -z "$GWTL_LINE" ]; then
|
|
echo "Error: Could not find getWasmTableEntry in $JS_FILE"
|
|
echo "The shims need to be inserted after getWasmTableEntry is defined"
|
|
rm "$SHIM_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
# Find the closing `};` after getWasmTableEntry (within next 10 lines)
|
|
INSERT_LINE=""
|
|
for ((i=GWTL_LINE; i<=GWTL_LINE+10; i++)); do
|
|
LINE_CONTENT=$(sed -n "${i}p" "$JS_FILE")
|
|
if [[ "$LINE_CONTENT" == "};" ]]; then
|
|
INSERT_LINE=$i
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$INSERT_LINE" ]; then
|
|
echo "Warning: Could not find closing }; for getWasmTableEntry"
|
|
echo "Inserting after line $GWTL_LINE"
|
|
INSERT_LINE=$GWTL_LINE
|
|
fi
|
|
|
|
echo "Injecting shims after line $INSERT_LINE..."
|
|
|
|
# Create output file with shims inserted
|
|
head -n "$INSERT_LINE" "$JS_FILE" > "${JS_FILE}.tmp"
|
|
cat "$SHIM_FILE" >> "${JS_FILE}.tmp"
|
|
tail -n +$((INSERT_LINE + 1)) "$JS_FILE" >> "${JS_FILE}.tmp"
|
|
|
|
# Replace original file
|
|
mv "${JS_FILE}.tmp" "$JS_FILE"
|
|
rm "$SHIM_FILE"
|
|
|
|
echo "Successfully injected $SIG_COUNT dynCall shims into $JS_FILE"
|
|
|
|
# Fix empty callback arrow functions generated by Emscripten with pthreads
|
|
# When pthreads is enabled, Emscripten generates empty {} for some direct-call paths
|
|
# because it assumes they won't be used. But they ARE used in certain cases.
|
|
# The dynCall_* functions exist (generated above), we just need to call them.
|
|
echo "Fixing empty callback arrow functions..."
|
|
|
|
TOTAL_FIXED=0
|
|
|
|
# Fix 1: HTML5 event callbacks (3 args) - signature iiii
|
|
# Pattern: ((a1, a2, a3) => {})(eventTypeId, ...
|
|
# Function pointer is 'callbackfunc' in these contexts
|
|
COUNT_BEFORE=$(grep -c '((a1, a2, a3) => {})(eventTypeId,' "$JS_FILE" || true)
|
|
if [ "$COUNT_BEFORE" -gt 0 ]; then
|
|
sed -i '' 's/((a1, a2, a3) => {})(eventTypeId,/((a1, a2, a3) => dynCall_iiii(callbackfunc, a1, a2, a3))(eventTypeId,/g' "$JS_FILE"
|
|
COUNT_AFTER=$(grep -c '((a1, a2, a3) => {})(eventTypeId,' "$JS_FILE" || true)
|
|
FIXED=$((COUNT_BEFORE - COUNT_AFTER))
|
|
echo " Fixed $FIXED HTML5 event callback(s) (dynCall_iiii)"
|
|
TOTAL_FIXED=$((TOTAL_FIXED + FIXED))
|
|
fi
|
|
|
|
# Fix 2: invokeEntryPoint pthread entry (1 arg) - signature ii (returns pointer)
|
|
# Pattern in invokeEntryPoint: var result = (a1 => {})(arg);
|
|
# Function pointer is 'ptr'
|
|
COUNT_BEFORE=$(grep -c 'var result = (a1 => {})(arg);' "$JS_FILE" || true)
|
|
if [ "$COUNT_BEFORE" -gt 0 ]; then
|
|
sed -i '' 's/var result = (a1 => {})(arg);/var result = dynCall_ii(ptr, arg);/g' "$JS_FILE"
|
|
COUNT_AFTER=$(grep -c 'var result = (a1 => {})(arg);' "$JS_FILE" || true)
|
|
FIXED=$((COUNT_BEFORE - COUNT_AFTER))
|
|
echo " Fixed $FIXED pthread entry callback(s) (dynCall_ii)"
|
|
TOTAL_FIXED=$((TOTAL_FIXED + FIXED))
|
|
fi
|
|
|
|
# Fix 3: ___call_sighandler (1 arg) - signature vi (void return)
|
|
# Pattern: return (a1 => {})(sig);
|
|
# Function pointer is 'fp'
|
|
COUNT_BEFORE=$(grep -c 'return (a1 => {})(sig);' "$JS_FILE" || true)
|
|
if [ "$COUNT_BEFORE" -gt 0 ]; then
|
|
sed -i '' 's/return (a1 => {})(sig);/return dynCall_vi(fp, sig);/g' "$JS_FILE"
|
|
COUNT_AFTER=$(grep -c 'return (a1 => {})(sig);' "$JS_FILE" || true)
|
|
FIXED=$((COUNT_BEFORE - COUNT_AFTER))
|
|
echo " Fixed $FIXED signal handler callback(s) (dynCall_vi)"
|
|
TOTAL_FIXED=$((TOTAL_FIXED + FIXED))
|
|
fi
|
|
|
|
# Fix 4: _emscripten_async_call timer (1 arg) - signature vi (void return)
|
|
# Pattern: var wrapper = () => (a1 => {})(arg);
|
|
# Function pointer is 'func'
|
|
COUNT_BEFORE=$(grep -c 'var wrapper = () => (a1 => {})(arg);' "$JS_FILE" || true)
|
|
if [ "$COUNT_BEFORE" -gt 0 ]; then
|
|
sed -i '' 's/var wrapper = () => (a1 => {})(arg);/var wrapper = () => dynCall_vi(func, arg);/g' "$JS_FILE"
|
|
COUNT_AFTER=$(grep -c 'var wrapper = () => (a1 => {})(arg);' "$JS_FILE" || true)
|
|
FIXED=$((COUNT_BEFORE - COUNT_AFTER))
|
|
echo " Fixed $FIXED async timer callback(s) (dynCall_vi)"
|
|
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
|
|
echo "No empty callbacks found - nothing to fix"
|
|
fi
|