Skip to content

Commit cf3770b

Browse files
committed
Emit bundler-friendly URL locators
This PR changes loading of main Wasm binary as well as helper Worker used by PThread integration to use a URL expression that can be both used directly by browsers as well as statically detected by bundlers like Webpack. The main caveats are: 1. Emscripten is very configurable, so some of the new conditions might look odd but are necessary to keep backward compatibility and allow overriding bundler-friendly URL with a custom one during runtime. 2. None of Closure, our fork of Terser, and `eval` (which is used by Emscripten's JS library preprocessing) support `import.meta` expressions without more work. While Closure seems to have _just_ implemented such support, and it wouldn't be too hard to add it to our Terser too, `eval` usage would still require a string replacement before execution (or complete revamp). To keep implementation simple, for now I went with just string replacement that covers all tools - this way, we replace `import.meta` -> `EMSCRIPTEN$IMPORT$META` only once per library when JS is added before any of this tooling is executed, and then replace back after everything is done right before the final emit. We might want to revisit this in future, but for now this works well and covers all the tooling incompatibilities together. 3. Webpack assumes that all modules are strict mode, so I updated `worker.js` correspondingly to avoid usages of global `this` (which is `undefined` in strict mode and breaks in bundled code) and instead using `self`; I've also updated the Node.js adapter code to satisfy strict requirements too and to be a bit simpler. 4. This won't work in Node.js, since it's not compatible with `EXPORT_ES6` in general yet. 5. I've only updated places for loading main Wasm binary and PThread code. This should cover majority of use-cases, but other external files like side modules, proxy-to-pthread, proxy-to-worker, external memory loading etc. are not covered by this PR and need to be updated separately if someone wants them to work with bundlers out of the box too. Fixes #13571.
1 parent 6e19063 commit cf3770b

File tree

7 files changed

+93
-60
lines changed

7 files changed

+93
-60
lines changed

emcc.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1206,7 +1206,8 @@ def phase_setup(state):
12061206
else:
12071207
target = 'a.out.js'
12081208

1209-
settings.TARGET_BASENAME = unsuffixed_basename(target)
1209+
settings.TARGET_BASENAME_WITH_EXT = os.path.basename(target)
1210+
settings.TARGET_BASENAME = unsuffixed(settings.TARGET_BASENAME_WITH_EXT)
12101211

12111212
if settings.EXTRA_EXPORTED_RUNTIME_METHODS:
12121213
diagnostics.warning('deprecated', 'EXTRA_EXPORTED_RUNTIME_METHODS is deprecated, please use EXPORTED_RUNTIME_METHODS instead')
@@ -2534,6 +2535,16 @@ def phase_final_emitting(options, target, wasm_target, memfile):
25342535
shared.JS.handle_license(final_js)
25352536
shared.run_process([shared.PYTHON, shared.path_from_root('tools', 'hacky_postprocess_around_closure_limitations.py'), final_js])
25362537

2538+
# Unmangle previously mangled `import.meta` references in both main code and libraries.
2539+
# See also: `preprocess` in parseTools.js.
2540+
if settings.EXPORT_ES6 and settings.USE_ES6_IMPORT_META:
2541+
with open(final_js, 'r+') as f:
2542+
src = f.read()
2543+
src = src.replace('EMSCRIPTEN$IMPORT$META', 'import.meta')
2544+
f.seek(0)
2545+
f.write(src)
2546+
f.truncate()
2547+
25372548
# Apply pre and postjs files
25382549
if options.extern_pre_js or options.extern_post_js:
25392550
logger.debug('applying extern pre/postjses')

src/closure-externs/closure-externs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
* The closure_compiler() method in tools/shared.py refers to this file when calling closure.
1212
*/
1313

14+
// Special placeholder for `import.meta`.
15+
var EMSCRIPTEN$IMPORT$META;
16+
1417
// Closure externs used by library_sockfs.js
1518

1619
/**

src/library_pthread.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,11 @@ var LibraryPThread = {
442442
// it could load up the same file. In that case, developer must either deliver the Blob
443443
// object in Module['mainScriptUrlOrBlob'], or a URL to it, so that pthread Workers can
444444
// independently load up the same main application file.
445-
'urlOrBlob': Module['mainScriptUrlOrBlob'] || _scriptDir,
445+
'urlOrBlob': Module['mainScriptUrlOrBlob']
446+
#if !EXPORT_ES6
447+
|| _scriptDir
448+
#endif
449+
,
446450
#if WASM2JS
447451
// the polyfill WebAssembly.Memory instance has function properties,
448452
// which will fail in postMessage, so just send a custom object with the
@@ -469,6 +473,17 @@ var LibraryPThread = {
469473
#if MINIMAL_RUNTIME
470474
var pthreadMainJs = Module['worker'];
471475
#else
476+
#if EXPORT_ES6 && USE_ES6_IMPORT_META
477+
// If we're using module output and there's no explicit override, use bundler-friendly pattern.
478+
if (!Module['locateFile']) {
479+
#if PTHREADS_DEBUG
480+
out('Allocating a new web worker from ' + new URL('{{{ PTHREAD_WORKER_FILE }}}', import.meta.url));
481+
#endif
482+
// Use bundler-friendly `new Worker(new URL(..., import.meta.url))` pattern; works in browsers too.
483+
PThread.unusedWorkers.push(new Worker(new URL('{{{ PTHREAD_WORKER_FILE }}}', import.meta.url)));
484+
return;
485+
}
486+
#endif
472487
// Allow HTML module to configure the location where the 'worker.js' file will be loaded from,
473488
// via Module.locateFile() function. If not specified, then the default URL 'worker.js' relative
474489
// to the main html file is loaded.

src/parseTools.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ function processMacros(text) {
3333
// Param filenameHint can be passed as a description to identify the file that is being processed, used
3434
// to locate errors for reporting and for html files to stop expansion between <style> and </style>.
3535
function preprocess(text, filenameHint) {
36+
if (EXPORT_ES6 && USE_ES6_IMPORT_META) {
37+
// `eval`, Terser and Closure don't support module syntax; to allow it,
38+
// we need to temporarily replace `import.meta` usages with placeholders
39+
// during preprocess phase, and back after all the other ops.
40+
// See also: `phase_final_emitting` in emcc.py.
41+
text = text.replace(/\bimport\.meta\b/g, 'EMSCRIPTEN$IMPORT$META');
42+
}
43+
3644
const IGNORE = 0;
3745
const SHOW = 1;
3846
// This state is entered after we have shown one of the block of an if/elif/else sequence.

src/preamble.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,10 +746,15 @@ function instrumentWasmTableWithAbort() {
746746
}
747747
#endif
748748

749+
#if EXPORT_ES6
750+
// Use bundler-friendly `new URL(..., import.meta.url)` pattern; works in browsers too.
751+
var wasmBinaryFile = new URL('{{{ WASM_BINARY_FILE }}}', import.meta.url).toString();
752+
#else
749753
var wasmBinaryFile = '{{{ WASM_BINARY_FILE }}}';
750754
if (!isDataURI(wasmBinaryFile)) {
751755
wasmBinaryFile = locateFile(wasmBinaryFile);
752756
}
757+
#endif
753758

754759
function getBinary(file) {
755760
try {
@@ -809,7 +814,7 @@ function getBinaryPromise() {
809814
}
810815
#endif
811816
}
812-
817+
813818
// Otherwise, getBinary should be able to get it synchronously
814819
return Promise.resolve().then(function() { return getBinary(wasmBinaryFile); });
815820
}

src/settings_internal.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ var SIDE_MODULE_IMPORTS = [];
3232
// stores the base name of the output file (-o TARGET_BASENAME.js)
3333
var TARGET_BASENAME = '';
3434

35+
// stores the base name of the output file with extension (TARGET_BASENAME.js or TARGET_BASENAME.mjs)
36+
var TARGET_BASENAME_WITH_EXT = '';
37+
3538
// Indicates that the syscalls (which we see statically) indicate that they need
3639
// full filesystem support. Otherwise, when just a small subset are used, we can
3740
// get away without including the full filesystem - in particular, if open() is

src/worker.js

Lines changed: 45 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,53 @@
88
// This is the entry point file that is loaded first by each Web Worker
99
// that executes pthreads on the Emscripten application.
1010

11+
'use strict';
12+
13+
var Module = {};
14+
15+
#if ENVIRONMENT_MAY_BE_NODE
16+
// Node.js support
17+
if (typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string') {
18+
// Create as web-worker-like an environment as we can.
19+
20+
var nodeWorkerThreads = require('worker_threads');
21+
22+
var parentPort = nodeWorkerThreads.parentPort;
23+
24+
parentPort.on('message', function(data) {
25+
onmessage({ data: data });
26+
});
27+
28+
var nodeFS = require('fs');
29+
30+
Object.assign(global, {
31+
self: global,
32+
require: require,
33+
Module: Module,
34+
location: {
35+
href: __filename
36+
},
37+
Worker: nodeWorkerThreads.Worker,
38+
importScripts: function(f) {
39+
(0, eval)(nodeFS.readFileSync(f, 'utf8'));
40+
},
41+
postMessage: function(msg) {
42+
parentPort.postMessage(msg);
43+
},
44+
performance: global.performance || {
45+
now: function() {
46+
return Date.now();
47+
}
48+
},
49+
});
50+
}
51+
#endif // ENVIRONMENT_MAY_BE_NODE
52+
1153
// Thread-local:
1254
#if EMBIND
1355
var initializedJS = false; // Guard variable for one-time init of the JS state (currently only embind types registration)
1456
#endif
1557

16-
var Module = {};
17-
1858
#if ASSERTIONS
1959
function assert(condition, text) {
2060
if (!condition) abort('Assertion failed: ' + text);
@@ -38,7 +78,7 @@ var out = function() {
3878
}
3979
#endif
4080
var err = threadPrintErr;
41-
this.alert = threadAlert;
81+
self.alert = threadAlert;
4282

4383
#if !MINIMAL_RUNTIME
4484
Module['instantiateWasm'] = function(info, receiveInstance) {
@@ -90,7 +130,7 @@ function moduleLoaded() {
90130
#endif
91131
}
92132

93-
this.onmessage = function(e) {
133+
self.onmessage = function(e) {
94134
try {
95135
if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
96136
#if MINIMAL_RUNTIME
@@ -128,7 +168,7 @@ this.onmessage = function(e) {
128168
#endif
129169

130170
#if MODULARIZE && EXPORT_ES6
131-
import(e.data.urlOrBlob).then(function({{{ EXPORT_NAME }}}) {
171+
(e.data.urlOrBlob ? import(e.data.urlOrBlob) : import('./{{{ TARGET_BASENAME_WITH_EXT }}}')).then(function({{{ EXPORT_NAME }}}) {
132172
return {{{ EXPORT_NAME }}}.default(Module);
133173
}).then(function(instance) {
134174
Module = instance;
@@ -295,55 +335,3 @@ this.onmessage = function(e) {
295335
throw ex;
296336
}
297337
};
298-
299-
#if ENVIRONMENT_MAY_BE_NODE
300-
// Node.js support
301-
if (typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string') {
302-
// Create as web-worker-like an environment as we can.
303-
self = {
304-
location: {
305-
href: __filename
306-
}
307-
};
308-
309-
var onmessage = this.onmessage;
310-
311-
var nodeWorkerThreads = require('worker_threads');
312-
313-
global.Worker = nodeWorkerThreads.Worker;
314-
315-
var parentPort = nodeWorkerThreads.parentPort;
316-
317-
parentPort.on('message', function(data) {
318-
onmessage({ data: data });
319-
});
320-
321-
var nodeFS = require('fs');
322-
323-
var nodeRead = function(filename) {
324-
return nodeFS.readFileSync(filename, 'utf8');
325-
};
326-
327-
function globalEval(x) {
328-
global.require = require;
329-
global.Module = Module;
330-
eval.call(null, x);
331-
}
332-
333-
importScripts = function(f) {
334-
globalEval(nodeRead(f));
335-
};
336-
337-
postMessage = function(msg) {
338-
parentPort.postMessage(msg);
339-
};
340-
341-
if (typeof performance === 'undefined') {
342-
performance = {
343-
now: function() {
344-
return Date.now();
345-
}
346-
};
347-
}
348-
}
349-
#endif // ENVIRONMENT_MAY_BE_NODE

0 commit comments

Comments
 (0)