Skip to content

Commit 5c0d413

Browse files
committed
Add an experimental way to export a "static" ES module.
Adds a new mode -sMODULARIZE=static which will change the output to be more of a static ES module. As mentioned in the docs, there will be a default async init function exported and named exports that correspond to Wasm and runtime exports. See the docs and test for an example. Internally, the module will now have an init function that wraps nearly all of the code except some top level variables that will be exported. When the init function is run, the top level variables are then updated which will in turn update the module exports. E.g. ``` async function init(moduleArgs) { function foo() {}; x_foo = foo; x_bar = wasmExports['bar']; } var x_foo, x_bar; export {x_foo as foo, x_bar as bar}; ``` Note: I alternatively tried to keep everything at the top level scope and move only the code that reads from moduleArg into an init function. This would make it possible to get rid of the `x_func` vars and directly add `export` to vars/functions we want to export. However, there are lots of things that read from moduleArg in many different spots and ways which makes this challenging.
1 parent 44c7540 commit 5c0d413

File tree

10 files changed

+140
-22
lines changed

10 files changed

+140
-22
lines changed

site/source/docs/tools_reference/settings_reference.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1947,7 +1947,23 @@ factory function, you can use --extern-pre-js or --extern-post-js. While
19471947
intended usage is to add code that is optimized with the rest of the emitted
19481948
code, allowing better dead code elimination and minification.
19491949

1950-
Default value: false
1950+
Experimental Feature - Static ES Modules:
1951+
1952+
Note this feature is still under active development and is subject to change!
1953+
1954+
To enable this feature use -sMODULARIZE=static. Enabling this mode will
1955+
produce an ES module that is a singleton with static ES module exports. The
1956+
module will export a default value that is an async init function and will
1957+
also export named values that correspond to the Wasm exports and runtime
1958+
exports. The init function must be called before any of the exports can be
1959+
used. An example of using the module is below.
1960+
1961+
import init, { foo, bar } from "./my_module.mjs"
1962+
await init(optionalArguments);
1963+
foo();
1964+
bar();
1965+
1966+
Default value: ''
19511967

19521968
.. _export_es6:
19531969

src/jsifier.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ function(${args}) {
644644
// asm module exports are done in emscripten.py, after the asm module is ready. Here
645645
// we also export library methods as necessary.
646646
if ((EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled)) && !isStub) {
647+
assert(MODULARIZE !== 'static', 'Exports in jsifier not currently supported with static modules.');
647648
contentText += `\nModule['${mangled}'] = ${mangled};`;
648649
}
649650
// Relocatable code needs signatures to create proper wrappers.

src/modules.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ function exportRuntime() {
384384
// If requested to be exported, export it. HEAP objects are exported
385385
// separately in updateMemoryViews
386386
if (EXPORTED_RUNTIME_METHODS.has(name) && !name.startsWith('HEAP')) {
387+
if (MODULARIZE === 'static') {
388+
return `x_${name} = ${name};`;
389+
}
387390
return `Module['${name}'] = ${name};`;
388391
}
389392
}

src/postamble_modularize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ moduleRtn = Module;
1818

1919
#endif // WASM_ASYNC_COMPILATION
2020

21-
#if ASSERTIONS
21+
#if ASSERTIONS && MODULARIZE != 'static'
2222
// Assertion for attempting to access module properties on the incoming
2323
// moduleArg. In the past we used this object as the prototype of the module
2424
// and assigned properties to it, but now we return a distinct object. This

src/runtime_shared.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@
2222
shouldExport = true;
2323
}
2424
}
25-
26-
return shouldExport ? `Module['${x}'] = ` : '';
25+
if (shouldExport) {
26+
if (MODULARIZE === 'static') {
27+
return `x_${x} = `
28+
}
29+
return `Module['${x}'] = `;
30+
}
31+
return '';
2732
};
2833
null;
2934
}}}

src/settings.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,25 @@ var DETERMINISTIC = false;
13251325
// --pre-js and --post-js happen to do that in non-MODULARIZE mode, their
13261326
// intended usage is to add code that is optimized with the rest of the emitted
13271327
// code, allowing better dead code elimination and minification.
1328+
//
1329+
// Experimental Feature - Static ES Modules:
1330+
//
1331+
// Note this feature is still under active development and is subject to change!
1332+
//
1333+
// To enable this feature use -sMODULARIZE=static. Enabling this mode will
1334+
// produce an ES module that is a singleton with static ES module exports. The
1335+
// module will export a default value that is an async init function and will
1336+
// also export named values that correspond to the Wasm exports and runtime
1337+
// exports. The init function must be called before any of the exports can be
1338+
// used. An example of using the module is below.
1339+
//
1340+
// import init, { foo, bar } from "./my_module.mjs"
1341+
// await init(optionalArguments);
1342+
// foo();
1343+
// bar();
1344+
//
13281345
// [link]
1329-
var MODULARIZE = false;
1346+
var MODULARIZE = '';
13301347

13311348
// Export using an ES6 Module export rather than a UMD export. MODULARIZE must
13321349
// be enabled for ES6 exports and is implicitly enabled if not already set.

test/test_other.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,35 @@ def test_export_es6(self, package_json, args):
442442

443443
self.assertContained('hello, world!', self.run_js('runner.mjs'))
444444

445+
@parameterized({
446+
'': ([],),
447+
'pthreads': (['-pthread'],),
448+
})
449+
def test_modularize_static(self, args):
450+
create_file('library.js', '''\
451+
addToLibrary({
452+
$baz: function() { console.log('baz'); }
453+
});''')
454+
self.run_process([EMCC, test_file('modularize_static.cpp'),
455+
'-sMODULARIZE=static',
456+
'-sEXPORTED_RUNTIME_METHODS=baz,addOnExit',
457+
'-sEXPORTED_FUNCTIONS=_bar,_main',
458+
'--js-library', 'library.js',
459+
'-o', 'modularize_static.mjs'] + args)
460+
461+
create_file('runner.mjs', '''
462+
import { strict as assert } from 'assert';
463+
import init, { _foo as foo, _bar as bar, baz, addOnExit, HEAP32 } from "./modularize_static.mjs";
464+
await init();
465+
foo(); // exported with EMSCRIPTEN_KEEPALIVE
466+
bar(); // exported with EXPORTED_FUNCTIONS
467+
baz(); // exported library function with EXPORTED_RUNTIME_METHODS
468+
assert(typeof addOnExit === 'function'); // exported runtime function with EXPORTED_RUNTIME_METHODS
469+
assert(typeof HEAP32 === 'object'); // exported runtime value by default
470+
''')
471+
472+
self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs'))
473+
445474
def test_emcc_out_file(self):
446475
# Verify that "-ofile" works in addition to "-o" "file"
447476
self.run_process([EMCC, '-c', '-ofoo.o', test_file('hello_world.c')])

tools/emscripten.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,12 @@ def install_wrapper(sym):
912912

913913
# TODO(sbc): Can we avoid exporting the dynCall_ functions on the module.
914914
should_export = settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS
915-
if name.startswith('dynCall_') or should_export:
916-
exported = "Module['%s'] = " % mangled
915+
if (name.startswith('dynCall_') and settings.MODULARIZE != 'static') or should_export:
916+
if settings.MODULARIZE == 'static':
917+
# Update the export declared at the top level.
918+
wrapper += f" x_{mangled} = "
919+
else:
920+
exported = "Module['%s'] = " % mangled
917921
else:
918922
exported = ''
919923
wrapper += exported

tools/link.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,13 @@ def phase_linker_setup(options, state, newargs):
757757

758758
if options.oformat == OFormat.MJS:
759759
settings.EXPORT_ES6 = 1
760-
settings.MODULARIZE = 1
760+
if not settings.MODULARIZE:
761+
settings.MODULARIZE = 1
762+
763+
if settings.MODULARIZE == 'static':
764+
diagnostics.warning('experimental', '-sMODULARIZE=static is still experimental. Many features may not work or will change.')
765+
if options.oformat != OFormat.MJS:
766+
exit_with_error('emcc: MODULARIZE static is only compatible with .mjs output files')
761767

762768
if options.oformat in (OFormat.WASM, OFormat.BARE):
763769
if options.emit_tsd:
@@ -2394,7 +2400,20 @@ def modularize():
23942400
if async_emit != '' and settings.EXPORT_NAME == 'config':
23952401
diagnostics.warning('emcc', 'EXPORT_NAME should not be named "config" when targeting Safari')
23962402

2397-
src = '''
2403+
if settings.MODULARIZE == 'static':
2404+
src = '''
2405+
export default async function init(moduleArg = {}) {
2406+
var moduleRtn;
2407+
2408+
%(src)s
2409+
2410+
return await moduleRtn;
2411+
}
2412+
''' % {
2413+
'src': src,
2414+
}
2415+
else:
2416+
src = '''
23982417
%(maybe_async)sfunction(moduleArg = {}) {
23992418
var moduleRtn;
24002419
@@ -2403,9 +2422,9 @@ def modularize():
24032422
return moduleRtn;
24042423
}
24052424
''' % {
2406-
'maybe_async': async_emit,
2407-
'src': src,
2408-
}
2425+
'maybe_async': async_emit,
2426+
'src': src,
2427+
}
24092428

24102429
if settings.MINIMAL_RUNTIME and not settings.PTHREADS:
24112430
# Single threaded MINIMAL_RUNTIME programs do not need access to
@@ -2424,19 +2443,31 @@ def modularize():
24242443
script_url = "typeof document != 'undefined' ? document.currentScript?.src : undefined"
24252444
if shared.target_environment_may_be('node'):
24262445
script_url_node = "if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;"
2427-
src = '''%(node_imports)s
2446+
if settings.MODULARIZE == 'static':
2447+
src = '''%(node_imports)s
2448+
var _scriptName = %(script_url)s;
2449+
%(script_url_node)s
2450+
%(src)s
2451+
''' % {
2452+
'node_imports': node_es6_imports(),
2453+
'script_url': script_url,
2454+
'script_url_node': script_url_node,
2455+
'src': src,
2456+
}
2457+
else:
2458+
src = '''%(node_imports)s
24282459
var %(EXPORT_NAME)s = (() => {
24292460
var _scriptName = %(script_url)s;
24302461
%(script_url_node)s
24312462
return (%(src)s);
24322463
})();
24332464
''' % {
2434-
'node_imports': node_es6_imports(),
2435-
'EXPORT_NAME': settings.EXPORT_NAME,
2436-
'script_url': script_url,
2437-
'script_url_node': script_url_node,
2438-
'src': src,
2439-
}
2465+
'node_imports': node_es6_imports(),
2466+
'EXPORT_NAME': settings.EXPORT_NAME,
2467+
'script_url': script_url,
2468+
'script_url_node': script_url_node,
2469+
'src': src,
2470+
}
24402471

24412472
# Given the async nature of how the Module function and Module object
24422473
# come into existence in AudioWorkletGlobalScope, store the Module
@@ -2448,9 +2479,18 @@ def modularize():
24482479
src += f'globalThis.AudioWorkletModule = {settings.EXPORT_NAME};\n'
24492480

24502481
# Export using a UMD style export, or ES6 exports if selected
2451-
if settings.EXPORT_ES6:
2482+
if settings.EXPORT_ES6 and settings.MODULARIZE != 'static':
24522483
src += 'export default %s;\n' % settings.EXPORT_NAME
24532484

2485+
if settings.MODULARIZE == 'static':
2486+
exports = settings.EXPORTED_FUNCTIONS + settings.EXPORTED_RUNTIME_METHODS
2487+
# Declare a top level var for each export so that code in the init function
2488+
# can assign to it and update the live module bindings.
2489+
src += "var " + ", ".join(['x_' + export for export in exports]) + ";\n"
2490+
# Export the functions with their original name.
2491+
exports = ['x_' + export + ' as ' + export for export in exports]
2492+
src += "export {" + ", ".join(exports) + "};\n"
2493+
24542494
elif not settings.MINIMAL_RUNTIME:
24552495
src += '''\
24562496
if (typeof exports === 'object' && typeof module === 'object')
@@ -2473,7 +2513,10 @@ def modularize():
24732513
elif settings.ENVIRONMENT_MAY_BE_NODE:
24742514
src += f'var isPthread = {node_pthread_detection()}\n'
24752515
src += '// When running as a pthread, construct a new instance on startup\n'
2476-
src += 'isPthread && %s();\n' % settings.EXPORT_NAME
2516+
if settings.MODULARIZE == 'static':
2517+
src += 'isPthread && init();\n'
2518+
else:
2519+
src += 'isPthread && %s();\n' % settings.EXPORT_NAME
24772520

24782521
final_js += '.modular.js'
24792522
write_file(final_js, src)

tools/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def __setattr__(self, name, value):
267267
self.attrs[name] = value
268268

269269
def check_type(self, name, value):
270-
if name in ('SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO'):
270+
if name in ('SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE'):
271271
return
272272
expected_type = self.types.get(name)
273273
if not expected_type:

0 commit comments

Comments
 (0)