pcbjam/scripts/common/inject-dyncall-shims.sh
Viktor Vaczi b46b4b69f1 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>
2026-03-13 12:26:48 +01:00

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