emscripten-core / emscripten-core/emscripten

BUILD_AS_WORKER: onmessage is installed too late under MODULARIZE, and is installed even on the main thread

Open
#27,625 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
27.6k
Forks
3.6k
Avg merge
1d 1h
Merged PRs (30d)
105

Description

`src/build_as_worker.js` wraps everything in an IIFE that installs the worker's `onmessage`:

```js
(() => {
var messageBuffer = null, buffer = 0;

function flushMessages() { ... }
function messageResender() { ... }

onmessage = (msg) => {
// if main has not yet been called (mem init file, other async things), buffer messages
if (!runtimeInitialized) {
if (!messageBuffer) {
messageBuffer = [];
setTimeout(messageResender, 100);
}
messageBuffer.push(msg);
return;
}
...
}
})();
```

Two problems follow from *when* and *where* that IIFE runs. They are independent, so I have described both here and am happy to split them.

### 1. The handler is installed even when the module is not running in a worker

The IIFE runs in whatever environment loads the module, and assigns `onmessage` unconditionally. A module linked with `-sBUILD_AS_WORKER` that also runs on the main thread therefore overwrites `window.onmessage`, taking over message delivery for the whole page.

That is not a quiet takeover. Any message the page receives from another source now reaches this handler, which either aborts:

```js
var func = Module['_' + msg.data['funcName']];
if (!func) abort('invalid worker function to call: ' + msg.data['funcName']);
```

or throws a `TypeError` outright, if `msg.data` is not an object with a `funcName` (a plain `postMessage('...')` from an embedding page or another library, for instance).

A guard at the top of the IIFE seems like the right fix:

```js
if (!ENVIRONMENT_IS_WORKER) return;
```

### 2. Under MODULARIZE, messages that arrive before the factory runs are dropped

With `-sMODULARIZE` the IIFE lives inside the module factory, which the application invokes whenever it is ready. A worker, however, starts receiving messages the moment it is created — so there is a window between "worker script began executing" and "factory was called" during which these messages arrive.

The existing buffering does not cover that window. `messageBuffer` only starts collecting from inside the handler this IIFE installs, so it protects the interval between the factory running and `runtimeInitialized` — not the earlier one.

The natural workaround is to install a small buffering `onmessage` in the worker bootstrap (or via `--pre-js`) that collects messages until the factory is called. That does not work either: this IIFE overwrites `onmessage` and starts from `messageBuffer = null`, so everything the earlier handler collected is discarded.

The net effect is that a MODULARIZE'd `BUILD_AS_WORKER` module silently loses any message sent before the application gets around to instantiating it — with no way for the application to bridge the gap.

What we ship locally is for the IIFE to adopt a pre-existing buffer if one is present:

```js
var messageBuffer = typeof workerMessageBuffer != 'undefined' ? workerMessageBuffer : null, buffer = 0;
// Upstream only starts the resender from inside its own handler, so an adopted
// buffer would otherwise never be flushed.
if (messageBuffer) setTimeout(messageResender, 100);
```

The bootstrap then just pushes into `workerMessageBuffer` until the factory is instantiated, and the module drains it. This has been running in production for us for a while.

I am raising this as an issue rather than sending the patch straight away, because the second half proposes a convention (`workerMessageBuffer`) that emscripten does not currently define, and you may well prefer a different shape — an explicit `Module` option, or having `build_as_worker.js` emit its own pre-factory bootstrap. Happy to send a PR for whichever you would like, and #1 can go on its own regardless.

Related, in that they are all variations of "the generated worker's `onmessage` is not reachable by the application": #20192, #11962, #8854.

### Version

emscripten 6.0.3; the same code is on `main` as of today.

Contributor guide

Open the contributing guide

Research direction

Start in src/build_as_worker.js and trace how its IIFE is emitted and invoked under MODULARIZE, including the worker bootstrap or --pre-js path described in the issue. Verify the behavior on a main thread and for messages sent before the factory runs. Done means non-worker pages retain their onmessage handler and pre-factory messages are preserved and delivered after initialization.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, wasm
Domain
build-system, compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.