Skip to content

Commit cc8efce

Browse files
authored
Add an experimental way to export a instance ES module. (#22867)
Adds a new mode `-sMODULARIZE=instance` 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. ```js 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 299be0b commit cc8efce

File tree

11 files changed

+184
-21
lines changed

11 files changed

+184
-21
lines changed

site/source/docs/tools_reference/settings_reference.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,6 +1947,22 @@ 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+
Experimental Feature - Instance 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=instance. Enabling this mode will
1955+
produce an ES module that is a singleton with 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+
19501966
Default value: false
19511967

19521968
.. _export_es6:

src/jsifier.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,11 @@ 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-
contentText += `\nModule['${mangled}'] = ${mangled};`;
647+
if (MODULARIZE === 'instance') {
648+
contentText += `\n__exp_${mangled} = ${mangled};`;
649+
} else {
650+
contentText += `\nModule['${mangled}'] = ${mangled};`;
651+
}
648652
}
649653
// Relocatable code needs signatures to create proper wrappers.
650654
if (sig && RELOCATABLE) {

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 === 'instance') {
388+
return `__exp_${name} = ${name};`;
389+
}
387390
return `Module['${name}'] = ${name};`;
388391
}
389392
}

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 === 'instance') {
27+
return `__exp_${x} = `
28+
}
29+
return `Module['${x}'] = `;
30+
}
31+
return '';
2732
};
2833
null;
2934
}}}

src/settings.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,23 @@ 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 - Instance 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=instance. Enabling this mode will
1334+
// produce an ES module that is a singleton with 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]
13291346
var MODULARIZE = false;
13301347

test/modularize_instance.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include <stdio.h>
2+
#include <emscripten.h>
3+
#ifdef __EMSCRIPTEN_PTHREADS__
4+
#include <pthread.h>
5+
#include <string.h>
6+
#endif
7+
8+
EMSCRIPTEN_KEEPALIVE void foo() {
9+
printf("foo\n");
10+
}
11+
12+
void bar() {
13+
printf("bar\n");
14+
}
15+
16+
void *thread_function(void *arg) {
17+
printf("main2\n");
18+
return NULL;
19+
}
20+
21+
int main() {
22+
printf("main1\n");
23+
#ifdef __EMSCRIPTEN_PTHREADS__
24+
pthread_t thread_id;
25+
int result = pthread_create(&thread_id, NULL, thread_function, NULL);
26+
if (result != 0) {
27+
fprintf(stderr, "Error creating thread: %s\n", strerror(result));
28+
return 1;
29+
}
30+
pthread_join(thread_id, NULL);
31+
#else
32+
printf("main2\n");
33+
#endif
34+
return 0;
35+
}

test/modularize_instance_runner.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import init, { _foo as foo } from "./modularize_static.mjs";
2+
await init();
3+
foo();

test/test_other.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,37 @@ def test_export_es6(self, package_json, args):
444444

445445
self.assertContained('hello, world!', self.run_js('runner.mjs'))
446446

447+
@parameterized({
448+
'': ([],),
449+
'pthreads': (['-pthread'],),
450+
})
451+
def test_modularize_instance(self, args):
452+
create_file('library.js', '''\
453+
addToLibrary({
454+
$baz: function() { console.log('baz'); },
455+
$qux: function() { console.log('qux'); }
456+
});''')
457+
self.run_process([EMCC, test_file('modularize_instance.c'),
458+
'-sMODULARIZE=instance',
459+
'-sEXPORTED_RUNTIME_METHODS=baz,addOnExit',
460+
'-sEXPORTED_FUNCTIONS=_bar,_main,qux',
461+
'--js-library', 'library.js',
462+
'-o', 'modularize_instance.mjs'] + args)
463+
464+
create_file('runner.mjs', '''
465+
import { strict as assert } from 'assert';
466+
import init, { _foo as foo, _bar as bar, baz, qux, addOnExit, HEAP32 } from "./modularize_instance.mjs";
467+
await init();
468+
foo(); // exported with EMSCRIPTEN_KEEPALIVE
469+
bar(); // exported with EXPORTED_FUNCTIONS
470+
baz(); // exported library function with EXPORTED_RUNTIME_METHODS
471+
qux(); // exported library function with EXPORTED_FUNCTIONS
472+
assert(typeof addOnExit === 'function'); // exported runtime function with EXPORTED_RUNTIME_METHODS
473+
assert(typeof HEAP32 === 'object'); // exported runtime value by default
474+
''')
475+
476+
self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs'))
477+
447478
def test_emcc_out_file(self):
448479
# Verify that "-ofile" works in addition to "-o" "file"
449480
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 != 'instance') or should_export:
916+
if settings.MODULARIZE == 'instance':
917+
# Update the export declared at the top level.
918+
wrapper += f" __exp_{mangled} = "
919+
else:
920+
exported = "Module['%s'] = " % mangled
917921
else:
918922
exported = ''
919923
wrapper += exported

tools/link.py

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,15 @@ def phase_linker_setup(options, state, newargs):
754754

755755
if options.oformat == OFormat.MJS:
756756
settings.EXPORT_ES6 = 1
757-
settings.MODULARIZE = 1
757+
default_setting('MODULARIZE', 1)
758+
759+
if settings.MODULARIZE and settings.MODULARIZE not in [1, 'instance']:
760+
exit_with_error(f'Invalid setting "{settings.MODULARIZE}" for MODULARIZE.')
761+
762+
if settings.MODULARIZE == 'instance':
763+
diagnostics.warning('experimental', '-sMODULARIZE=instance is still experimental. Many features may not work or will change.')
764+
if options.oformat != OFormat.MJS:
765+
exit_with_error('emcc: MODULARIZE instance is only compatible with .mjs output files')
758766

759767
if options.oformat in (OFormat.WASM, OFormat.BARE):
760768
if options.emit_tsd:
@@ -2391,7 +2399,20 @@ def modularize():
23912399
if async_emit != '' and settings.EXPORT_NAME == 'config':
23922400
diagnostics.warning('emcc', 'EXPORT_NAME should not be named "config" when targeting Safari')
23932401

2394-
src = '''
2402+
if settings.MODULARIZE == 'instance':
2403+
src = '''
2404+
export default async function init(moduleArg = {}) {
2405+
var moduleRtn;
2406+
2407+
%(src)s
2408+
2409+
return await moduleRtn;
2410+
}
2411+
''' % {
2412+
'src': src,
2413+
}
2414+
else:
2415+
src = '''
23952416
%(maybe_async)sfunction(moduleArg = {}) {
23962417
var moduleRtn;
23972418
@@ -2400,9 +2421,9 @@ def modularize():
24002421
return moduleRtn;
24012422
}
24022423
''' % {
2403-
'maybe_async': async_emit,
2404-
'src': src,
2405-
}
2424+
'maybe_async': async_emit,
2425+
'src': src,
2426+
}
24062427

24072428
if settings.MINIMAL_RUNTIME and not settings.PTHREADS:
24082429
# Single threaded MINIMAL_RUNTIME programs do not need access to
@@ -2421,19 +2442,31 @@ def modularize():
24212442
script_url = "typeof document != 'undefined' ? document.currentScript?.src : undefined"
24222443
if shared.target_environment_may_be('node'):
24232444
script_url_node = "if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;"
2424-
src = '''%(node_imports)s
2445+
if settings.MODULARIZE == 'instance':
2446+
src = '''%(node_imports)s
2447+
var _scriptName = %(script_url)s;
2448+
%(script_url_node)s
2449+
%(src)s
2450+
''' % {
2451+
'node_imports': node_es6_imports(),
2452+
'script_url': script_url,
2453+
'script_url_node': script_url_node,
2454+
'src': src,
2455+
}
2456+
else:
2457+
src = '''%(node_imports)s
24252458
var %(EXPORT_NAME)s = (() => {
24262459
var _scriptName = %(script_url)s;
24272460
%(script_url_node)s
24282461
return (%(src)s);
24292462
})();
24302463
''' % {
2431-
'node_imports': node_es6_imports(),
2432-
'EXPORT_NAME': settings.EXPORT_NAME,
2433-
'script_url': script_url,
2434-
'script_url_node': script_url_node,
2435-
'src': src,
2436-
}
2464+
'node_imports': node_es6_imports(),
2465+
'EXPORT_NAME': settings.EXPORT_NAME,
2466+
'script_url': script_url,
2467+
'script_url_node': script_url_node,
2468+
'src': src,
2469+
}
24372470

24382471
# Given the async nature of how the Module function and Module object
24392472
# come into existence in AudioWorkletGlobalScope, store the Module
@@ -2446,8 +2479,16 @@ def modularize():
24462479

24472480
# Export using a UMD style export, or ES6 exports if selected
24482481
if settings.EXPORT_ES6:
2449-
src += 'export default %s;\n' % settings.EXPORT_NAME
2450-
2482+
if settings.MODULARIZE == 'instance':
2483+
exports = settings.EXPORTED_FUNCTIONS + settings.EXPORTED_RUNTIME_METHODS
2484+
# Declare a top level var for each export so that code in the init function
2485+
# can assign to it and update the live module bindings.
2486+
src += 'var ' + ', '.join(['__exp_' + export for export in exports]) + ';\n'
2487+
# Export the functions with their original name.
2488+
exports = ['__exp_' + export + ' as ' + export for export in exports]
2489+
src += 'export {' + ', '.join(exports) + '};\n'
2490+
else:
2491+
src += 'export default %s;\n' % settings.EXPORT_NAME
24512492
elif not settings.MINIMAL_RUNTIME:
24522493
src += '''\
24532494
if (typeof exports === 'object' && typeof module === 'object')
@@ -2470,7 +2511,10 @@ def modularize():
24702511
elif settings.ENVIRONMENT_MAY_BE_NODE:
24712512
src += f'var isPthread = {node_pthread_detection()}\n'
24722513
src += '// When running as a pthread, construct a new instance on startup\n'
2473-
src += 'isPthread && %s();\n' % settings.EXPORT_NAME
2514+
if settings.MODULARIZE == 'instance':
2515+
src += 'isPthread && init();\n'
2516+
else:
2517+
src += 'isPthread && %s();\n' % settings.EXPORT_NAME
24742518

24752519
final_js += '.modular.js'
24762520
write_file(final_js, src)

tools/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ 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+
# These settings have a variable type so cannot be easily type checked.
271+
if name in ('SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE'):
271272
return
272273
expected_type = self.types.get(name)
273274
if not expected_type:

0 commit comments

Comments
 (0)