mailbox S1: shim delivery tick, embind audit, app-side WasmMailbox

Shim: mailbox FIFO + self-armed delivery tick calling wxWasmMailboxTick
(plain export — never inside a pump's awaited ccall); injector sentinel
fixed (the old marker also matched evtloop's EM_JS probe text). Doc 18:
79-export embind audit (14+3 production mutators to wrap, 20 pure-read
allowlist, asymmetries). web/standalone WasmMailbox: FIFO defer-until-
settled keyed on the proxy-safe kicadOpenFileBusy probe, 7 vitest green.
Dual-variant wx battery green (28+39+7 both variants). Bump wxwidgets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
This commit is contained in:
Gergő Törcsvári 2026-08-05 14:16:33 +02:00
commit 61b5f266fb
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
7 changed files with 419 additions and 15 deletions

View file

@ -109,7 +109,9 @@ fi
# the __wxSchedulerInstalled marker. Default OFF — the legacy build is the
# shippable fallback until S5.
if [ "${WX_SCHEDULER:-0}" = "1" ]; then
if grep -q '__wxSchedulerInstalled' "$JS_FILE"; then
# NOTE: must be the shim-source sentinel, not __wxSchedulerInstalled — that
# string also appears in evtloop.cpp's EM_JS probe inside every glue.
if grep -q '__WX_SCHEDULER_SHIM_SOURCE__' "$JS_FILE"; then
echo "asyncify-scheduler already present - skipping"
else
echo "" >> "$JS_FILE"

View file

@ -1,18 +1,74 @@
// === AsyncifyScheduler (S0 scaffolding — observation-only) ===
// === AsyncifyScheduler (S1 — mailbox front-end live; scheduler core lands in S2) ===
// __WX_SCHEDULER_SHIM_SOURCE__ — injector idempotence sentinel. Must appear ONLY in
// this file: the obvious marker (__wxSchedulerInstalled) also occurs in evtloop.cpp's
// EM_JS probe text inside every glue, which made the injector skip real injections.
// docs/features/async/17-mailbox-scheduler-plan.md · injected only on WX_SCHEDULER=1 builds.
//
// S0 contract: this file changes NO runtime behavior. It claims the namespace, the
// registry data structures, and the build marker so (a) the dual-glue build variant
// exists and can run the full suite, and (b) tests can detect which runtime they're on.
// S2 turns this into the sole owner of Asyncify.currData/state (doc 13 §1): the four
// hooks (handleSleep wrap, fiber-swap tracking, trampoline ownership, deferred drain)
// land there, gated on the N1 single-writer tripwire. Until then the legacy
// handlesleep.js shim (injected just above) stays authoritative.
// S1 state: the MAILBOX is live — deferred browser callbacks (wx timers, via
// wx/wasm/private/mailbox.h) are enqueued here on expiry and delivered by the wx
// event pump (evtloop.cpp wxWasmMailboxDeliver) from a clean dispatch context,
// only when the dispatch interlock is free. The legacy handlesleep.js shim
// (injected just above) remains authoritative for currData until S2, when the
// scheduler core (registry, deferred drain, fiber tracking) lands here gated on
// the N1 single-writer tripwire.
if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
globalThis.__wxSchedulerInstalled = true;
Asyncify.__schedulerBuild = 1;
var AsyncifyScheduler = {
// --- S1 mailbox -------------------------------------------------------
// Due messages, FIFO. {fn, arg} are wasm function-pointer / pointer ints;
// the C side (wxWasmMailboxDeliver) pops and calls them. Exactly-once:
// a message stays queued until the pump delivers it — there is no drop
// path, matching the emscripten_async_call contract it replaces.
mailbox: [],
enqueued: 0,
delivered: 0,
_tickArmed: false,
enqueueAfter: function (fn, arg, ms) {
var self = this;
setTimeout(function () {
self.mailbox.push({ fn: fn, arg: arg });
self.enqueued++;
self._armDeliveryTick();
}, ms);
},
pop: function () {
var m = this.mailbox.shift();
if (m) this.delivered++;
return m || null;
},
// Deliver via a dedicated PLAIN export call (wxWasmMailboxTick), never
// from inside a pump's awaited ProcessEvents ccall — a fiber swap there
// sits on the JS-awaits-a-suspending-export boundary (#13302) and traps.
// Re-arms at 17ms while messages remain (the interlock may be held; the
// C side skips delivery then). One retry loop for the WHOLE queue — this
// replaces the per-timer retry storms of the legacy path.
_armDeliveryTick: function () {
if (this._tickArmed) return;
this._tickArmed = true;
var self = this;
setTimeout(function tick() {
try {
if (Module["_wxWasmMailboxTick"]) Module["_wxWasmMailboxTick"]();
} catch (e) {
self._tickArmed = false;
// Mirror wxWasmScheduleProcessEvents' guard: a trapped delivery
// must not leave the interlock held or a parked nested DoRun stuck.
if (Module["_wx_dispatch_abandon"]) Module["_wx_dispatch_abandon"]();
var exits = Module["_wxNestedLoopExit"];
if (exits && exits.length) (exits.pop())();
throw e;
}
if (self.mailbox.length > 0) {
setTimeout(tick, 17);
} else {
self._tickArmed = false;
}
}, 0);
},
// --- S2 scheduler core (not yet live) ---------------------------------
// ctx = { id, kind: 'main'|'modal'|'nested'|'coroutine'|'sleep',
// buffer, status: 'running'|'parked'|'ready', wakeReason, result }
contexts: new Map(),
@ -21,22 +77,24 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
transitionRunning: false,
trampolineRunning: false,
// S2 fills these in. They throw today so a premature caller is loud, not silent —
// nothing in an S0 build calls them.
// S2 fills these in. They throw today so a premature caller is loud, not
// silent — nothing in an S1 build calls them.
park: function () { throw new Error("[wx-scheduler] park(): not implemented until S2"); },
resume: function () { throw new Error("[wx-scheduler] resume(): not implemented until S2"); },
drain: function () { throw new Error("[wx-scheduler] drain(): not implemented until S2"); },
state: function () {
return "[wx-scheduler] build=1 impl=S0-observation-only"
return "[wx-scheduler] build=1 impl=S1-mailbox"
+ " mailbox=" + this.mailbox.length
+ " enqueued=" + this.enqueued
+ " delivered=" + this.delivered
+ " contexts=" + this.contexts.size
+ " ready=" + this.readyQueue.length
+ " running=" + this.running
+ " transition=" + this.transitionRunning;
},
};
globalThis.__wxScheduler = AsyncifyScheduler;
console.log("[wx-scheduler] scaffolding installed (S0, observation-only)");
console.log("[wx-scheduler] scaffolding installed (S1, mailbox live)");
}
// === End AsyncifyScheduler ===