emscripten-core / emscripten-core/emscripten
BUILD_AS_WORKER: onmessage is installed too late under MODULARIZE, and is installed even on the main thread
- 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
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