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
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 ===
|
||||
Loading…
Reference in a new issue