fix(wasm): bind dynCall_* to real DYNCALLS=1 exports; refactor shim into files
The shim bound bare dynCall_* names to JS getWasmTableEntry() calls, bypassing the asyncify-instrumented dynCall_* wasm trampolines that -sDYNCALLS=1 provides. That broke Asyncify unwind/rewind through indirect calls -> "indirect call signature mismatch" (caught every frame in Firefox; fatal renderer crash in Chrome). Bind the bare names to wasmExports["dynCall_<sig>"] instead. Result: the PCBnew "select draw lines" e2e is green in Firefox (tool selects and draws, zero page errors). Dropped the fiber-stabilization block, the shipped diagnostic block, and the exportCallStack JS hack (all compensated for the wrong binding); shim shrank 521 -> ~250 lines. - scripts/common/inject-dyncall-shims.sh: orchestrator only; injected JS extracted to scripts/common/shims/ - scripts/common/shims/dyncall-binding.js.tmpl: per-signature binding template - scripts/common/shims/handlesleep.js: nested-Asyncify handleSleep fix (#9153) - scripts/common/shims/diagnostics.js: logging-only, opt-in via SHIM_DIAGNOSTICS=1 - tests/package.json: add test:kicad:firefox / test:kicad:chrome scripts Known issue (tracked separately): Chrome still renderer-crashes on the first coroutine resume. Asyncify.doRewind replays the deep main-context call stack and exceeds V8's execution-stack limit (Firefox tolerates the same wasm). Proper fix is JSPI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9a04217788
commit
a4ad69412e
5 changed files with 259 additions and 466 deletions
|
|
@ -1,521 +1,150 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Inject dynCall_* shims into Emscripten-generated JS
|
# Post-process Emscripten-generated pcbnew.js for KiCad WASM.
|
||||||
#
|
#
|
||||||
# Emscripten 4.x no longer generates signature-specific dynCall_* functions,
|
# The actual JavaScript that gets injected lives in readable, standalone files in
|
||||||
# but the invoke_* wrappers (for C++ exception handling) still call them.
|
# scripts/common/shims/ (not inline heredocs):
|
||||||
# This script auto-generates shims using getWasmTableEntry() which IS defined.
|
# - dyncall-binding.js.tmpl per-signature dynCall_<sig> binding (templated)
|
||||||
|
# - handlesleep.js nested-Asyncify handleSleep currData save/restore (#9153)
|
||||||
|
# - diagnostics.js optional logging-only instrumentation (see SHIM_DIAGNOSTICS)
|
||||||
#
|
#
|
||||||
# Usage: inject-dyncall-shims.sh <pcbnew.js>
|
# Why bind dynCall_* to the real wasm exports: the build links -sDYNCALLS=1, so
|
||||||
|
# asyncify-INSTRUMENTED dynCall_* trampolines exist as wasm exports. The bare
|
||||||
|
# dynCall_<sig>(index, ...) call sites (invoke_* wrappers, the empty-callback fixes
|
||||||
|
# below) aren't bound to top-level names, so we bind them to wasmExports["dynCall_<sig>"].
|
||||||
|
# Binding to getWasmTableEntry() instead bypasses the instrumentation and breaks
|
||||||
|
# unwind/rewind through indirect calls ("indirect call signature mismatch" — caught
|
||||||
|
# every frame in Firefox; a hard renderer crash in Chrome/V8).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# inject-dyncall-shims.sh <pcbnew.js>
|
||||||
|
# SHIM_DIAGNOSTICS=1 inject-dyncall-shims.sh <pcbnew.js> # also inject diagnostics.js
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
JS_FILE="$1"
|
JS_FILE="$1"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SHIM_DIR="$SCRIPT_DIR/shims"
|
||||||
|
|
||||||
|
# One-line toggle for the diagnostics module (default OFF).
|
||||||
|
SHIM_DIAGNOSTICS="${SHIM_DIAGNOSTICS:-0}"
|
||||||
|
|
||||||
if [ -z "$JS_FILE" ] || [ ! -f "$JS_FILE" ]; then
|
if [ -z "$JS_FILE" ] || [ ! -f "$JS_FILE" ]; then
|
||||||
echo "Error: JS file not found: $JS_FILE"
|
echo "Error: JS file not found: $JS_FILE"
|
||||||
echo "Usage: $0 <path/to/pcbnew.js>"
|
echo "Usage: $0 <path/to/pcbnew.js>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
for f in dyncall-binding.js.tmpl handlesleep.js diagnostics.js; do
|
||||||
|
if [ ! -f "$SHIM_DIR/$f" ]; then
|
||||||
|
echo "Error: missing shim source $SHIM_DIR/$f"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- 1. dynCall_<sig> bindings -------------------------------------------------
|
||||||
echo "Extracting dynCall signatures from $JS_FILE..."
|
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_//')
|
SIGNATURES=$(grep -oE 'dynCall_[a-zA-Z0-9]+' "$JS_FILE" | sort -u | sed 's/dynCall_//')
|
||||||
|
|
||||||
if [ -z "$SIGNATURES" ]; then
|
if [ -z "$SIGNATURES" ]; then
|
||||||
echo "No dynCall signatures found - nothing to inject"
|
echo "No dynCall signatures found - nothing to inject"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SIG_COUNT=$(echo "$SIGNATURES" | wc -l | tr -d ' ')
|
SIG_COUNT=$(echo "$SIGNATURES" | wc -l | tr -d ' ')
|
||||||
echo "Found $SIG_COUNT unique signatures"
|
echo "Found $SIG_COUNT unique signatures"
|
||||||
|
|
||||||
# Generate shim code
|
# The template body is everything from the `function` line onward (skip its comments).
|
||||||
SHIM_FILE=$(mktemp)
|
TEMPLATE_BODY=$(sed -n '/^function /,$p' "$SHIM_DIR/dyncall-binding.js.tmpl")
|
||||||
cat > "$SHIM_FILE" << 'HEADER'
|
|
||||||
|
|
||||||
// === dynCall shims for Emscripten exception handling ===
|
SHIM_FILE=$(mktemp)
|
||||||
// Auto-generated: maps dynCall_SIG() calls to getWasmTableEntry()
|
{
|
||||||
// This fixes "dynCall_* is not defined" errors in Emscripten 4.x
|
echo ""
|
||||||
//
|
echo "// === dynCall bindings (bind bare names to the real DYNCALLS=1 wasm exports) ==="
|
||||||
// These shims are asyncify-aware: they track Asyncify.exportCallStack so that
|
} > "$SHIM_FILE"
|
||||||
// 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
|
for sig in $SIGNATURES; do
|
||||||
# Count args: signature length - 1 (first char is return type)
|
|
||||||
argcount=$((${#sig} - 1))
|
argcount=$((${#sig} - 1))
|
||||||
|
|
||||||
# Generate argument list: index, a0, a1, a2, ...
|
|
||||||
args="index"
|
args="index"
|
||||||
call_args=""
|
call_args=""
|
||||||
for ((i=0; i<argcount; i++)); do
|
for ((i=0; i<argcount; i++)); do
|
||||||
args="$args, a$i"
|
args="$args, a$i"
|
||||||
if [ $i -gt 0 ]; then
|
if [ $i -gt 0 ]; then call_args="$call_args, "; fi
|
||||||
call_args="$call_args, "
|
|
||||||
fi
|
|
||||||
call_args="${call_args}a$i"
|
call_args="${call_args}a$i"
|
||||||
done
|
done
|
||||||
|
echo "$TEMPLATE_BODY" \
|
||||||
cat >> "$SHIM_FILE" << SHIMEOF
|
| sed -e "s/@SIG@/$sig/g" -e "s/@ARGS@/$args/g" -e "s/@CALLARGS@/$call_args/g" \
|
||||||
function dynCall_$sig($args) {
|
>> "$SHIM_FILE"
|
||||||
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
|
done
|
||||||
|
echo "// === End dynCall bindings ===" >> "$SHIM_FILE"
|
||||||
|
|
||||||
echo "" >> "$SHIM_FILE"
|
# Insert right after the getWasmTableEntry definition.
|
||||||
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)
|
GWTL_LINE=$(grep -n '^var getWasmTableEntry = funcPtr => {' "$JS_FILE" | head -1 | cut -d: -f1)
|
||||||
|
|
||||||
if [ -z "$GWTL_LINE" ]; then
|
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)
|
GWTL_LINE=$(grep -n 'var getWasmTableEntry' "$JS_FILE" | head -1 | cut -d: -f1)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$GWTL_LINE" ]; then
|
if [ -z "$GWTL_LINE" ]; then
|
||||||
echo "Error: Could not find getWasmTableEntry in $JS_FILE"
|
echo "Error: Could not find getWasmTableEntry in $JS_FILE"; rm "$SHIM_FILE"; exit 1
|
||||||
echo "The shims need to be inserted after getWasmTableEntry is defined"
|
|
||||||
rm "$SHIM_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Find the closing `};` after getWasmTableEntry (within next 10 lines)
|
|
||||||
INSERT_LINE=""
|
INSERT_LINE=""
|
||||||
for ((i=GWTL_LINE; i<=GWTL_LINE+10; i++)); do
|
for ((i=GWTL_LINE; i<=GWTL_LINE+10; i++)); do
|
||||||
LINE_CONTENT=$(sed -n "${i}p" "$JS_FILE")
|
[ "$(sed -n "${i}p" "$JS_FILE")" == "};" ] && { INSERT_LINE=$i; break; }
|
||||||
if [[ "$LINE_CONTENT" == "};" ]]; then
|
|
||||||
INSERT_LINE=$i
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
[ -z "$INSERT_LINE" ] && INSERT_LINE=$GWTL_LINE
|
||||||
|
|
||||||
if [ -z "$INSERT_LINE" ]; then
|
echo "Injecting dynCall bindings after line $INSERT_LINE..."
|
||||||
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"
|
head -n "$INSERT_LINE" "$JS_FILE" > "${JS_FILE}.tmp"
|
||||||
cat "$SHIM_FILE" >> "${JS_FILE}.tmp"
|
cat "$SHIM_FILE" >> "${JS_FILE}.tmp"
|
||||||
tail -n +$((INSERT_LINE + 1)) "$JS_FILE" >> "${JS_FILE}.tmp"
|
tail -n +$((INSERT_LINE + 1)) "$JS_FILE" >> "${JS_FILE}.tmp"
|
||||||
|
|
||||||
# Replace original file
|
|
||||||
mv "${JS_FILE}.tmp" "$JS_FILE"
|
mv "${JS_FILE}.tmp" "$JS_FILE"
|
||||||
rm "$SHIM_FILE"
|
rm "$SHIM_FILE"
|
||||||
|
echo "Injected $SIG_COUNT dynCall bindings"
|
||||||
|
|
||||||
echo "Successfully injected $SIG_COUNT dynCall shims into $JS_FILE"
|
# --- 2. Empty-callback fixes ---------------------------------------------------
|
||||||
|
# Emscripten+pthreads emits some direct-call paths as no-op ((a1)=>{}) stubs that
|
||||||
# Stabilize Asyncify rewind targets for Emscripten fibers.
|
# ARE used. Rewire each to the (now-bound) dynCall_*. (Kept inline: simple sed one-liners.)
|
||||||
# When a fiber resumes via rewind, the outer JS frame can collapse to the
|
|
||||||
# innermost dynCall wrapper on the next yield. Giving each fiber its own stable
|
|
||||||
# rewind wrapper preserves the original re-entry target across later yields.
|
|
||||||
if grep -q 'Fiber rewind stabilization for Asyncify fibers' "$JS_FILE"; then
|
|
||||||
echo "Fiber rewind stabilization already present - skipping"
|
|
||||||
else
|
|
||||||
FIBER_PATCH_FILE=$(mktemp)
|
|
||||||
cat > "$FIBER_PATCH_FILE" << 'FIBERPATCH'
|
|
||||||
|
|
||||||
// === Fiber rewind stabilization for Asyncify fibers ===
|
|
||||||
if (typeof Asyncify !== "undefined" && typeof Fibers !== "undefined" && typeof _emscripten_fiber_swap !== "undefined") {
|
|
||||||
var __originalAsyncifySetDataRewindFunc = Asyncify.setDataRewindFunc.bind(Asyncify);
|
|
||||||
Asyncify.setDataRewindFunc = function(ptr, forcedBottomOfCallStack) {
|
|
||||||
if (forcedBottomOfCallStack) {
|
|
||||||
var rewindId = Asyncify.getCallStackId(forcedBottomOfCallStack);
|
|
||||||
GROWABLE_HEAP_I32()[(((ptr) + (8)) >> 2)] = rewindId;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return __originalAsyncifySetDataRewindFunc(ptr);
|
|
||||||
};
|
|
||||||
|
|
||||||
Fibers.rewindTargetByFiber ||= {};
|
|
||||||
Fibers.rewindWrapperByFiber ||= {};
|
|
||||||
Fibers.entryWrapperByFiber ||= {};
|
|
||||||
Fibers.entryKeyByFiber ||= {};
|
|
||||||
Fibers.shouldUseStableRewindTarget ||= function(fiber) {
|
|
||||||
return GROWABLE_HEAP_U32()[(((fiber) + (16)) >> 2)] !== 0;
|
|
||||||
};
|
|
||||||
Fibers.ensureRewindWrapper ||= function(fiber) {
|
|
||||||
var key = "__fiber_rewind_" + fiber;
|
|
||||||
|
|
||||||
if (!Fibers.rewindWrapperByFiber[fiber]) {
|
|
||||||
var target = Fibers.rewindTargetByFiber[fiber];
|
|
||||||
|
|
||||||
Fibers.rewindWrapperByFiber[fiber] = function() {
|
|
||||||
Asyncify.exportCallStack.push(key);
|
|
||||||
try {
|
|
||||||
return wasmExports[target]();
|
|
||||||
} finally {
|
|
||||||
if (!ABORT) {
|
|
||||||
Asyncify.exportCallStack.pop();
|
|
||||||
Asyncify.maybeStopUnwind();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
wasmExports[key] = Fibers.rewindWrapperByFiber[fiber];
|
|
||||||
}
|
|
||||||
|
|
||||||
return key;
|
|
||||||
};
|
|
||||||
|
|
||||||
var __originalFinishContextSwitch = Fibers.finishContextSwitch;
|
|
||||||
Fibers.finishContextSwitch = function(newFiber) {
|
|
||||||
var entryPoint = GROWABLE_HEAP_U32()[(((newFiber) + (12)) >> 2)];
|
|
||||||
|
|
||||||
if (entryPoint !== 0 && !Fibers.entryWrapperByFiber[newFiber]) {
|
|
||||||
var userData = GROWABLE_HEAP_U32()[(((newFiber) + (16)) >> 2)];
|
|
||||||
var entryKey = "__fiber_entry_" + newFiber;
|
|
||||||
|
|
||||||
Fibers.entryWrapperByFiber[newFiber] = function() {
|
|
||||||
return dynCall_vi(entryPoint, userData);
|
|
||||||
};
|
|
||||||
Fibers.entryKeyByFiber[newFiber] = entryKey;
|
|
||||||
wasmExports[entryKey] = Fibers.entryWrapperByFiber[newFiber];
|
|
||||||
}
|
|
||||||
|
|
||||||
return __originalFinishContextSwitch(newFiber);
|
|
||||||
};
|
|
||||||
|
|
||||||
var __originalEmscriptenFiberSwap = _emscripten_fiber_swap;
|
|
||||||
_emscripten_fiber_swap = (oldFiber, newFiber) => {
|
|
||||||
if (ABORT) return;
|
|
||||||
|
|
||||||
if (Asyncify.state === Asyncify.State.Normal) {
|
|
||||||
Asyncify.state = Asyncify.State.Unwinding;
|
|
||||||
|
|
||||||
var asyncifyData = oldFiber + 20;
|
|
||||||
if (Fibers.shouldUseStableRewindTarget(oldFiber)) {
|
|
||||||
if (!Fibers.rewindTargetByFiber[oldFiber]) {
|
|
||||||
Fibers.rewindTargetByFiber[oldFiber] = Fibers.entryKeyByFiber[oldFiber] || Asyncify.exportCallStack[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
var rewindKey = Fibers.ensureRewindWrapper(oldFiber);
|
|
||||||
Asyncify.setDataRewindFunc(asyncifyData, rewindKey);
|
|
||||||
} else {
|
|
||||||
Asyncify.setDataRewindFunc(asyncifyData);
|
|
||||||
}
|
|
||||||
Asyncify.currData = asyncifyData;
|
|
||||||
_asyncify_start_unwind(asyncifyData);
|
|
||||||
|
|
||||||
var stackTop = stackSave();
|
|
||||||
GROWABLE_HEAP_U32()[(((oldFiber) + (8)) >> 2)] = stackTop;
|
|
||||||
|
|
||||||
Fibers.nextFiber = newFiber;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return __originalEmscriptenFiberSwap(oldFiber, newFiber);
|
|
||||||
};
|
|
||||||
|
|
||||||
_emscripten_fiber_swap.isAsync = true;
|
|
||||||
|
|
||||||
// --- handleSleep save/restore for nested Asyncify ---
|
|
||||||
// Asyncify.currData is a single-slot global. When a fiber swap runs inside an
|
|
||||||
// EM_ASYNC_JS Promise await (e.g., wxDialog::ShowModal via startModal), the
|
|
||||||
// fiber swap overwrites currData with the fiber's asyncify_data, losing the
|
|
||||||
// sleep's own buffer. On Promise resolution, handleSleep's doRewind then uses
|
|
||||||
// the wrong buffer and crashes with "index out of bounds" or "unreachable".
|
|
||||||
//
|
|
||||||
// Workaround: intercept Asyncify.allocateData to record which pointer belongs
|
|
||||||
// to the active handleSleep; restore it to Asyncify.currData inside the wakeUp
|
|
||||||
// callback before handleSleep proceeds to _asyncify_start_rewind + doRewind.
|
|
||||||
// Documented upstream as Emscripten Issue #9153 (wontfix).
|
|
||||||
if (typeof Asyncify.handleSleep === "function"
|
|
||||||
&& typeof Asyncify.allocateData === "function"
|
|
||||||
&& !Asyncify.__nestedHandleSleepInstalled) {
|
|
||||||
// Stack of handleSleep contexts awaiting their allocateData association.
|
|
||||||
// Each context learns its data pointer when allocateData fires.
|
|
||||||
Asyncify.__pendingSleepContexts = [];
|
|
||||||
|
|
||||||
var __originalAllocateData = Asyncify.allocateData.bind(Asyncify);
|
|
||||||
Asyncify.allocateData = function() {
|
|
||||||
var ptr = __originalAllocateData();
|
|
||||||
// Associate with the innermost pending handleSleep that hasn't been
|
|
||||||
// linked yet. allocateData is called once per sleep's unwind, so the
|
|
||||||
// innermost un-linked context is ours.
|
|
||||||
for (var i = Asyncify.__pendingSleepContexts.length - 1; i >= 0; --i) {
|
|
||||||
var ctx = Asyncify.__pendingSleepContexts[i];
|
|
||||||
if (!ctx.capturedData) {
|
|
||||||
ctx.capturedData = ptr;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
var __originalHandleSleep = Asyncify.handleSleep.bind(Asyncify);
|
|
||||||
Asyncify.handleSleep = function(startAsync) {
|
|
||||||
var sleepCtx = { capturedData: null, cleanedUp: false };
|
|
||||||
Asyncify.__pendingSleepContexts.push(sleepCtx);
|
|
||||||
|
|
||||||
var cleanup = function() {
|
|
||||||
if (sleepCtx.cleanedUp) return;
|
|
||||||
sleepCtx.cleanedUp = true;
|
|
||||||
var idx = Asyncify.__pendingSleepContexts.indexOf(sleepCtx);
|
|
||||||
if (idx !== -1) Asyncify.__pendingSleepContexts.splice(idx, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
return __originalHandleSleep(function(wakeUp) {
|
|
||||||
return startAsync(function(result) {
|
|
||||||
// wakeUp runs from pure JS on Promise resolution. Fiber swaps
|
|
||||||
// during the await may have overwritten Asyncify.currData with
|
|
||||||
// a fiber's buffer pointer. Restore OUR buffer so handleSleep's
|
|
||||||
// _asyncify_start_rewind and doRewind use the right data.
|
|
||||||
if (sleepCtx.capturedData) {
|
|
||||||
Asyncify.currData = sleepCtx.capturedData;
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
return wakeUp(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
cleanup();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Asyncify.__nestedHandleSleepInstalled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// === End fiber rewind stabilization ===
|
|
||||||
FIBERPATCH
|
|
||||||
|
|
||||||
FIBER_INSERT_LINE=$(grep -n '^_emscripten_fiber_swap\.isAsync = true;$' "$JS_FILE" | head -1 | cut -d: -f1)
|
|
||||||
|
|
||||||
if [ -z "$FIBER_INSERT_LINE" ]; then
|
|
||||||
echo "Error: Could not find _emscripten_fiber_swap.isAsync marker in $JS_FILE"
|
|
||||||
rm "$FIBER_PATCH_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Injecting fiber rewind stabilization after line $FIBER_INSERT_LINE..."
|
|
||||||
|
|
||||||
head -n "$FIBER_INSERT_LINE" "$JS_FILE" > "${JS_FILE}.tmp"
|
|
||||||
cat "$FIBER_PATCH_FILE" >> "${JS_FILE}.tmp"
|
|
||||||
tail -n +$((FIBER_INSERT_LINE + 1)) "$JS_FILE" >> "${JS_FILE}.tmp"
|
|
||||||
|
|
||||||
mv "${JS_FILE}.tmp" "$JS_FILE"
|
|
||||||
rm "$FIBER_PATCH_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 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..."
|
echo "Fixing empty callback arrow functions..."
|
||||||
|
|
||||||
TOTAL_FIXED=0
|
TOTAL_FIXED=0
|
||||||
|
apply_fix() { # <grep/sed pattern> <sed replacement> <label>
|
||||||
|
local before; before=$(grep -c "$1" "$JS_FILE" || true)
|
||||||
|
if [ "$before" -gt 0 ]; then
|
||||||
|
sed -i '' "s/$1/$2/g" "$JS_FILE"
|
||||||
|
local after; after=$(grep -c "$1" "$JS_FILE" || true)
|
||||||
|
echo " Fixed $((before - after)) $3"
|
||||||
|
TOTAL_FIXED=$((TOTAL_FIXED + before - after))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
apply_fix '((a1, a2, a3) => {})(eventTypeId,' '((a1, a2, a3) => dynCall_iiii(callbackfunc, a1, a2, a3))(eventTypeId,' "HTML5 event callback(s) (dynCall_iiii)"
|
||||||
|
apply_fix 'var result = (a1 => {})(arg);' 'var result = dynCall_ii(ptr, arg);' "pthread entry callback(s) (dynCall_ii)"
|
||||||
|
apply_fix 'return (a1 => {})(sig);' 'return dynCall_vi(fp, sig);' "signal handler callback(s) (dynCall_vi)"
|
||||||
|
apply_fix 'var wrapper = () => (a1 => {})(arg);' 'var wrapper = () => dynCall_vi(func, arg);' "async timer callback(s) (dynCall_vi)"
|
||||||
|
apply_fix 'var iterFunc = (() => {});' 'var iterFunc = () => dynCall_v(func);' "main loop callback(s) (dynCall_v)"
|
||||||
|
apply_fix '(a1 => {})(userData);' 'dynCall_vi(entryPoint, userData);' "fiber entry callback(s) (dynCall_vi)"
|
||||||
|
echo "Total: Fixed $TOTAL_FIXED empty callback(s)"
|
||||||
|
|
||||||
# Fix 1: HTML5 event callbacks (3 args) - signature iiii
|
# --- 3. Nested-Asyncify handleSleep fix ---------------------------------------
|
||||||
# Pattern: ((a1, a2, a3) => {})(eventTypeId, ...
|
# Injected after Emscripten's fiber glue (the _emscripten_fiber_swap.isAsync marker).
|
||||||
# Function pointer is 'callbackfunc' in these contexts
|
if grep -q '__nestedHandleSleepInstalled' "$JS_FILE"; then
|
||||||
COUNT_BEFORE=$(grep -c '((a1, a2, a3) => {})(eventTypeId,' "$JS_FILE" || true)
|
echo "handleSleep fix already present - skipping"
|
||||||
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
|
|
||||||
|
|
||||||
# Fix 6: Asyncify fiber entry callback (1 arg) - signature vi (void return)
|
|
||||||
# Pattern in Fibers.finishContextSwitch: (a1 => {})(userData);
|
|
||||||
# Function pointer is 'entryPoint'
|
|
||||||
COUNT_BEFORE=$(grep -c '(a1 => {})(userData);' "$JS_FILE" || true)
|
|
||||||
if [ "$COUNT_BEFORE" -gt 0 ]; then
|
|
||||||
sed -i '' 's/(a1 => {})(userData);/dynCall_vi(entryPoint, userData);/g' "$JS_FILE"
|
|
||||||
COUNT_AFTER=$(grep -c '(a1 => {})(userData);' "$JS_FILE" || true)
|
|
||||||
FIXED=$((COUNT_BEFORE - COUNT_AFTER))
|
|
||||||
echo " Fixed $FIXED fiber entry callback(s) (dynCall_vi)"
|
|
||||||
TOTAL_FIXED=$((TOTAL_FIXED + FIXED))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$TOTAL_FIXED" -gt 0 ]; then
|
|
||||||
echo "Total: Fixed $TOTAL_FIXED empty callback(s)"
|
|
||||||
else
|
else
|
||||||
echo "No empty callbacks found - nothing to fix"
|
HS_MARKER=$(grep -n '^_emscripten_fiber_swap\.isAsync = true;$' "$JS_FILE" | head -1 | cut -d: -f1)
|
||||||
|
if [ -z "$HS_MARKER" ]; then
|
||||||
|
echo "Warning: _emscripten_fiber_swap.isAsync marker not found - skipping handleSleep fix"
|
||||||
|
else
|
||||||
|
head -n "$HS_MARKER" "$JS_FILE" > "${JS_FILE}.tmp"
|
||||||
|
echo "" >> "${JS_FILE}.tmp"
|
||||||
|
cat "$SHIM_DIR/handlesleep.js" >> "${JS_FILE}.tmp"
|
||||||
|
tail -n +$((HS_MARKER + 1)) "$JS_FILE" >> "${JS_FILE}.tmp"
|
||||||
|
mv "${JS_FILE}.tmp" "$JS_FILE"
|
||||||
|
echo "Injected handleSleep fix after line $HS_MARKER"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# === Diagnostic logging for modal/asyncify/fiber interactions ===
|
# --- 4. Optional diagnostics (logging only) -----------------------------------
|
||||||
# This instruments key JS functions to trace "index out of bounds" crashes
|
if [ "$SHIM_DIAGNOSTICS" = "1" ]; then
|
||||||
# that occur after EndModal during Asyncify rewind.
|
if grep -q 'DIAG] Asyncify/fiber/modal diagnostics installed' "$JS_FILE"; then
|
||||||
echo "Injecting diagnostic logging for modal/asyncify interactions..."
|
echo "diagnostics already present - skipping"
|
||||||
|
else
|
||||||
DIAG_PATCH_FILE=$(mktemp)
|
echo "" >> "$JS_FILE"
|
||||||
cat > "$DIAG_PATCH_FILE" << 'DIAGPATCH'
|
cat "$SHIM_DIR/diagnostics.js" >> "$JS_FILE"
|
||||||
|
echo "Appended diagnostics module (SHIM_DIAGNOSTICS=1)"
|
||||||
// === Diagnostic: modal/asyncify/fiber interaction logging ===
|
fi
|
||||||
(function() {
|
else
|
||||||
var _diagTimerId = 0;
|
echo "diagnostics disabled (set SHIM_DIAGNOSTICS=1 to enable)"
|
||||||
var _diagModalActive = false;
|
fi
|
||||||
var _diagScheduledTimers = {};
|
|
||||||
|
|
||||||
// 1. Instrument _emscripten_async_call to log function pointer validity
|
|
||||||
if (typeof _emscripten_async_call !== "undefined") {
|
|
||||||
var __diag_orig_async_call = _emscripten_async_call;
|
|
||||||
_emscripten_async_call = function(func, arg, millis) {
|
|
||||||
var tableSize = wasmTable ? wasmTable.length : -1;
|
|
||||||
var inBounds = func < tableSize;
|
|
||||||
var id = ++_diagTimerId;
|
|
||||||
console.warn('[DIAG_ASYNC_CALL] id=' + id + ' func=' + func + ' arg=' + arg +
|
|
||||||
' millis=' + millis + ' tableSize=' + tableSize +
|
|
||||||
' inBounds=' + inBounds + ' modalActive=' + _diagModalActive +
|
|
||||||
' asyncifyState=' + (typeof Asyncify !== 'undefined' ? Asyncify.state : 'N/A'));
|
|
||||||
if (!inBounds) {
|
|
||||||
console.error('[DIAG_ASYNC_CALL] ALREADY OUT OF BOUNDS at schedule time! func=' + func);
|
|
||||||
console.trace();
|
|
||||||
}
|
|
||||||
_diagScheduledTimers[id] = { func: func, arg: arg, millis: millis, scheduledDuringModal: _diagModalActive };
|
|
||||||
return __diag_orig_async_call(func, arg, millis);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Instrument Asyncify.setDataRewindFunc to log rewind target changes
|
|
||||||
if (typeof Asyncify !== "undefined" && Asyncify.setDataRewindFunc) {
|
|
||||||
var __diag_orig_setRewind = Asyncify.setDataRewindFunc.bind(Asyncify);
|
|
||||||
Asyncify.setDataRewindFunc = function(ptr, forcedBottomOfCallStack) {
|
|
||||||
console.warn('[DIAG_REWIND_FUNC] ptr=' + ptr + ' forced=' + forcedBottomOfCallStack +
|
|
||||||
' state=' + Asyncify.state + ' modalActive=' + _diagModalActive +
|
|
||||||
' callStack=' + JSON.stringify(Asyncify.exportCallStack));
|
|
||||||
return __diag_orig_setRewind(ptr, forcedBottomOfCallStack);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Track modal lifecycle via _endModal
|
|
||||||
// startModal sets Module._endModal; we wrap that setter to detect modal start/end.
|
|
||||||
var __diag_origDefineProperty = Object.defineProperty;
|
|
||||||
var __diag_endModalSet = false;
|
|
||||||
// Instead of defineProperty (complex), poll-wrap: patch the endModal assignment
|
|
||||||
// by wrapping startModal's promise resolution. We detect modal start when
|
|
||||||
// _endModal appears on Module, and modal end when it's deleted.
|
|
||||||
if (typeof Module !== "undefined") {
|
|
||||||
var __diag_checkInterval = setInterval(function() {
|
|
||||||
if (Module._endModal && !__diag_endModalSet) {
|
|
||||||
__diag_endModalSet = true;
|
|
||||||
_diagModalActive = true;
|
|
||||||
console.warn('[DIAG_MODAL] Modal started (Module._endModal appeared)' +
|
|
||||||
' asyncifyState=' + (typeof Asyncify !== 'undefined' ? Asyncify.state : 'N/A'));
|
|
||||||
|
|
||||||
var __diag_origEndModal = Module._endModal;
|
|
||||||
Module._endModal = function(code) {
|
|
||||||
console.warn('[DIAG_MODAL] EndModal called with code=' + code +
|
|
||||||
' asyncifyState=' + (typeof Asyncify !== 'undefined' ? Asyncify.state : 'N/A'));
|
|
||||||
_diagModalActive = false;
|
|
||||||
return __diag_origEndModal(code);
|
|
||||||
};
|
|
||||||
} else if (!Module._endModal && __diag_endModalSet) {
|
|
||||||
__diag_endModalSet = false;
|
|
||||||
console.warn('[DIAG_MODAL] Modal cleanup complete (Module._endModal deleted)' +
|
|
||||||
' asyncifyState=' + (typeof Asyncify !== 'undefined' ? Asyncify.state : 'N/A'));
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Instrument dynCall_vi to catch the exact out-of-bounds call
|
|
||||||
if (typeof dynCall_vi !== "undefined") {
|
|
||||||
var __diag_orig_dynCall_vi = dynCall_vi;
|
|
||||||
dynCall_vi = function(funcPtr, arg1) {
|
|
||||||
var tableSize = wasmTable ? wasmTable.length : -1;
|
|
||||||
if (funcPtr >= tableSize || funcPtr < 0) {
|
|
||||||
console.error('[DIAG_DYNCALL_VI] OUT OF BOUNDS! funcPtr=' + funcPtr +
|
|
||||||
' tableSize=' + tableSize + ' arg1=' + arg1 +
|
|
||||||
' modalActive=' + _diagModalActive +
|
|
||||||
' asyncifyState=' + (typeof Asyncify !== 'undefined' ? Asyncify.state : 'N/A'));
|
|
||||||
console.trace();
|
|
||||||
return; // Skip the call to prevent crash, let execution continue
|
|
||||||
}
|
|
||||||
return __diag_orig_dynCall_vi(funcPtr, arg1);
|
|
||||||
};
|
|
||||||
dynCall_vi.isAsync = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn('[DIAG] Modal/asyncify diagnostic logging installed');
|
|
||||||
})();
|
|
||||||
// === End diagnostic logging ===
|
|
||||||
DIAGPATCH
|
|
||||||
|
|
||||||
# Append diagnostic patch at the end of the JS file
|
|
||||||
cat "$DIAG_PATCH_FILE" >> "$JS_FILE"
|
|
||||||
rm "$DIAG_PATCH_FILE"
|
|
||||||
echo "Diagnostic logging injected"
|
|
||||||
|
|
|
||||||
83
scripts/common/shims/diagnostics.js
Normal file
83
scripts/common/shims/diagnostics.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// === Asyncify / fiber / modal diagnostics (LOGGING ONLY — no behavior change) ===
|
||||||
|
//
|
||||||
|
// Injected only when inject-dyncall-shims.sh runs with SHIM_DIAGNOSTICS=1.
|
||||||
|
// Observability for the Chrome/V8 renderer crash on the first coroutine resume
|
||||||
|
// (the main-context Asyncify rewind). Does NOT swallow or alter any call — it
|
||||||
|
// logs and traces, then delegates, so the real crash still happens and can be
|
||||||
|
// observed right up to the faulting point.
|
||||||
|
(function() {
|
||||||
|
var modalActive = false;
|
||||||
|
var tableLen = function() { return (typeof wasmTable !== "undefined" && wasmTable) ? wasmTable.length : -1; };
|
||||||
|
var asyncState = function() { return (typeof Asyncify !== "undefined") ? Asyncify.state : "N/A"; };
|
||||||
|
|
||||||
|
// 1. Timer scheduling — flag function pointers already out of bounds at schedule time.
|
||||||
|
if (typeof _emscripten_async_call !== "undefined") {
|
||||||
|
var __origAsyncCall = _emscripten_async_call;
|
||||||
|
_emscripten_async_call = function(func, arg, millis) {
|
||||||
|
var inBounds = func >= 0 && func < tableLen();
|
||||||
|
console.warn("[DIAG_ASYNC_CALL] func=" + func + " arg=" + arg + " millis=" + millis +
|
||||||
|
" inBounds=" + inBounds + " modalActive=" + modalActive + " state=" + asyncState());
|
||||||
|
if (!inBounds) { console.error("[DIAG_ASYNC_CALL] OUT OF BOUNDS at schedule time! func=" + func); console.trace(); }
|
||||||
|
return __origAsyncCall(func, arg, millis);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Rewind target selection — which export Asyncify will re-enter on rewind.
|
||||||
|
if (typeof Asyncify !== "undefined" && Asyncify.setDataRewindFunc) {
|
||||||
|
var __origSetRewind = Asyncify.setDataRewindFunc.bind(Asyncify);
|
||||||
|
Asyncify.setDataRewindFunc = function(ptr, forced) {
|
||||||
|
console.warn("[DIAG_REWIND_FUNC] ptr=" + ptr + " forced=" + forced + " state=" + Asyncify.state +
|
||||||
|
" modalActive=" + modalActive + " callStack=" + JSON.stringify(Asyncify.exportCallStack));
|
||||||
|
return __origSetRewind(ptr, forced);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. doRewind — the actual rewind that crashes V8. Log the buffer + saved rewind id
|
||||||
|
// immediately before re-entering wasm, so the last line before the crash names it.
|
||||||
|
if (typeof Asyncify !== "undefined" && typeof Asyncify.doRewind === "function") {
|
||||||
|
var __origDoRewind = Asyncify.doRewind.bind(Asyncify);
|
||||||
|
Asyncify.doRewind = function(ptr) {
|
||||||
|
var rewindId = -1;
|
||||||
|
try { rewindId = (typeof GROWABLE_HEAP_I32 === "function") ? GROWABLE_HEAP_I32()[((ptr + 8) >> 2)] : HEAP32[((ptr + 8) >> 2)]; } catch (e) {}
|
||||||
|
console.warn("[DIAG_DOREWIND] ptr=" + ptr + " rewindId=" + rewindId + " state=" + asyncState() +
|
||||||
|
" modalActive=" + modalActive + " — re-entering wasm now");
|
||||||
|
return __origDoRewind(ptr);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. dynCall_vi — log (and trace) out-of-bounds pointers but DO NOT swallow; call through.
|
||||||
|
if (typeof dynCall_vi === "function") {
|
||||||
|
var __origDynCallVi = dynCall_vi;
|
||||||
|
dynCall_vi = function(index, a0) {
|
||||||
|
if (index < 0 || index >= tableLen()) {
|
||||||
|
console.error("[DIAG_DYNCALL_VI] OUT OF BOUNDS index=" + index + " tableLen=" + tableLen() +
|
||||||
|
" modalActive=" + modalActive + " state=" + asyncState());
|
||||||
|
console.trace();
|
||||||
|
}
|
||||||
|
return __origDynCallVi(index, a0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Modal lifecycle (startModal sets Module._endModal; detect appear/disappear).
|
||||||
|
if (typeof Module !== "undefined") {
|
||||||
|
var seen = false;
|
||||||
|
setInterval(function() {
|
||||||
|
if (Module._endModal && !seen) {
|
||||||
|
seen = true; modalActive = true;
|
||||||
|
console.warn("[DIAG_MODAL] modal started, state=" + asyncState());
|
||||||
|
var __origEnd = Module._endModal;
|
||||||
|
Module._endModal = function(code) {
|
||||||
|
console.warn("[DIAG_MODAL] EndModal code=" + code + " state=" + asyncState());
|
||||||
|
modalActive = false;
|
||||||
|
return __origEnd(code);
|
||||||
|
};
|
||||||
|
} else if (!Module._endModal && seen) {
|
||||||
|
seen = false;
|
||||||
|
console.warn("[DIAG_MODAL] modal cleanup, state=" + asyncState());
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("[DIAG] Asyncify/fiber/modal diagnostics installed (logging only)");
|
||||||
|
})();
|
||||||
|
// === End diagnostics ===
|
||||||
12
scripts/common/shims/dyncall-binding.js.tmpl
Normal file
12
scripts/common/shims/dyncall-binding.js.tmpl
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Template for one dynCall_<sig> binding. Placeholders substituted per signature
|
||||||
|
// by inject-dyncall-shims.sh: @SIG@ = signature, @ARGS@ = "index, a0, a1, ...",
|
||||||
|
// @CALLARGS@ = "a0, a1, ..." (the args without the function-pointer index).
|
||||||
|
//
|
||||||
|
// Binds the bare name to the REAL asyncify-instrumented wasm export
|
||||||
|
// (wasmExports["dynCall_<sig>"], present because the build links -sDYNCALLS=1).
|
||||||
|
// Falls back to getWasmTableEntry only if no such export exists.
|
||||||
|
function dynCall_@SIG@(@ARGS@) {
|
||||||
|
var f = (typeof wasmExports !== 'undefined') && wasmExports["dynCall_@SIG@"];
|
||||||
|
if (f) return f(@ARGS@);
|
||||||
|
return getWasmTableEntry(index)(@CALLARGS@);
|
||||||
|
}
|
||||||
67
scripts/common/shims/handlesleep.js
Normal file
67
scripts/common/shims/handlesleep.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// === Nested-Asyncify handleSleep currData save/restore (Emscripten #9153) ===
|
||||||
|
//
|
||||||
|
// Asyncify.currData is a single-slot global. When a fiber swap runs inside an
|
||||||
|
// EM_ASYNC_JS Promise await (e.g., wxDialog::ShowModal via startModal), the
|
||||||
|
// fiber swap overwrites currData with the fiber's asyncify_data, losing the
|
||||||
|
// sleep's own buffer. On Promise resolution, handleSleep's doRewind then uses
|
||||||
|
// the wrong buffer and crashes with "index out of bounds" or "unreachable".
|
||||||
|
//
|
||||||
|
// Workaround: intercept Asyncify.allocateData to record which pointer belongs to
|
||||||
|
// the active handleSleep; restore it to Asyncify.currData inside the wakeUp
|
||||||
|
// callback before handleSleep proceeds to _asyncify_start_rewind + doRewind.
|
||||||
|
if (typeof Asyncify !== "undefined") {
|
||||||
|
if (typeof Asyncify.handleSleep === "function"
|
||||||
|
&& typeof Asyncify.allocateData === "function"
|
||||||
|
&& !Asyncify.__nestedHandleSleepInstalled) {
|
||||||
|
// Stack of handleSleep contexts awaiting their allocateData association.
|
||||||
|
Asyncify.__pendingSleepContexts = [];
|
||||||
|
|
||||||
|
var __originalAllocateData = Asyncify.allocateData.bind(Asyncify);
|
||||||
|
Asyncify.allocateData = function() {
|
||||||
|
var ptr = __originalAllocateData();
|
||||||
|
// Associate with the innermost pending handleSleep not yet linked.
|
||||||
|
for (var i = Asyncify.__pendingSleepContexts.length - 1; i >= 0; --i) {
|
||||||
|
var ctx = Asyncify.__pendingSleepContexts[i];
|
||||||
|
if (!ctx.capturedData) {
|
||||||
|
ctx.capturedData = ptr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
var __originalHandleSleep = Asyncify.handleSleep.bind(Asyncify);
|
||||||
|
Asyncify.handleSleep = function(startAsync) {
|
||||||
|
var sleepCtx = { capturedData: null, cleanedUp: false };
|
||||||
|
Asyncify.__pendingSleepContexts.push(sleepCtx);
|
||||||
|
|
||||||
|
var cleanup = function() {
|
||||||
|
if (sleepCtx.cleanedUp) return;
|
||||||
|
sleepCtx.cleanedUp = true;
|
||||||
|
var idx = Asyncify.__pendingSleepContexts.indexOf(sleepCtx);
|
||||||
|
if (idx !== -1) Asyncify.__pendingSleepContexts.splice(idx, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return __originalHandleSleep(function(wakeUp) {
|
||||||
|
return startAsync(function(result) {
|
||||||
|
// wakeUp runs from pure JS on Promise resolution. Fiber swaps during
|
||||||
|
// the await may have overwritten Asyncify.currData. Restore OUR buffer
|
||||||
|
// so handleSleep's _asyncify_start_rewind and doRewind use it.
|
||||||
|
if (sleepCtx.capturedData) {
|
||||||
|
Asyncify.currData = sleepCtx.capturedData;
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
return wakeUp(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
cleanup();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Asyncify.__nestedHandleSleepInstalled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// === End nested-Asyncify handleSleep fix ===
|
||||||
|
|
@ -9,8 +9,10 @@
|
||||||
"build-wasm": "cd apps && make -f Makefile.wasm",
|
"build-wasm": "cd apps && make -f Makefile.wasm",
|
||||||
"serve": "npx serve apps -p 8080 -c ../serve.json",
|
"serve": "npx serve apps -p 8080 -c ../serve.json",
|
||||||
"setup:kicad": "./scripts/setup-kicad-wasm.sh",
|
"setup:kicad": "./scripts/setup-kicad-wasm.sh",
|
||||||
"test:kicad": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox --headed",
|
"test:kicad:firefox": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=firefox",
|
||||||
"test:kicad:headed": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed"
|
"test:kicad:chrome": "npm run setup:kicad && playwright test --config=playwright-kicad.config.ts --project=chromium --headed",
|
||||||
|
"test:kicad": "npm run test:kicad:firefox",
|
||||||
|
"test:kicad:headed": "npm run test:kicad:chrome"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.40.0",
|
"@playwright/test": "^1.40.0",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue