Skip to content

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

Description

@tovabar

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

(() => {
  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:

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:

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:

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions