diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 1cd56541b8f6a..b8a971f917e71 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1947,6 +1947,22 @@ factory function, you can use --extern-pre-js or --extern-post-js. While intended usage is to add code that is optimized with the rest of the emitted code, allowing better dead code elimination and minification. +Experimental Feature - Instance ES Modules: + +Note this feature is still under active development and is subject to change! + +To enable this feature use -sMODULARIZE=instance. Enabling this mode will +produce an ES module that is a singleton with ES module exports. The +module will export a default value that is an async init function and will +also export named values that correspond to the Wasm exports and runtime +exports. The init function must be called before any of the exports can be +used. An example of using the module is below. + + import init, { foo, bar } from "./my_module.mjs" + await init(optionalArguments); + foo(); + bar(); + Default value: false .. _export_es6: diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 233928161ccd3..b9a4ae1a5f4e0 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -644,7 +644,11 @@ function(${args}) { // asm module exports are done in emscripten.py, after the asm module is ready. Here // we also export library methods as necessary. if ((EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled)) && !isStub) { - contentText += `\nModule['${mangled}'] = ${mangled};`; + if (MODULARIZE === 'instance') { + contentText += `\n__exp_${mangled} = ${mangled};`; + } else { + contentText += `\nModule['${mangled}'] = ${mangled};`; + } } // Relocatable code needs signatures to create proper wrappers. if (sig && RELOCATABLE) { diff --git a/src/modules.mjs b/src/modules.mjs index eea19009a01d5..bed0931d88ef6 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -384,6 +384,9 @@ function exportRuntime() { // If requested to be exported, export it. HEAP objects are exported // separately in updateMemoryViews if (EXPORTED_RUNTIME_METHODS.has(name) && !name.startsWith('HEAP')) { + if (MODULARIZE === 'instance') { + return `__exp_${name} = ${name};`; + } return `Module['${name}'] = ${name};`; } } diff --git a/src/runtime_shared.js b/src/runtime_shared.js index 9a9943c3ad13e..22ef5e881c426 100644 --- a/src/runtime_shared.js +++ b/src/runtime_shared.js @@ -22,8 +22,13 @@ shouldExport = true; } } - - return shouldExport ? `Module['${x}'] = ` : ''; + if (shouldExport) { + if (MODULARIZE === 'instance') { + return `__exp_${x} = ` + } + return `Module['${x}'] = `; + } + return ''; }; null; }}} diff --git a/src/settings.js b/src/settings.js index d42eb6d6c9fb7..e009a98c907e7 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1325,6 +1325,23 @@ var DETERMINISTIC = false; // --pre-js and --post-js happen to do that in non-MODULARIZE mode, their // intended usage is to add code that is optimized with the rest of the emitted // code, allowing better dead code elimination and minification. +// +// Experimental Feature - Instance ES Modules: +// +// Note this feature is still under active development and is subject to change! +// +// To enable this feature use -sMODULARIZE=instance. Enabling this mode will +// produce an ES module that is a singleton with ES module exports. The +// module will export a default value that is an async init function and will +// also export named values that correspond to the Wasm exports and runtime +// exports. The init function must be called before any of the exports can be +// used. An example of using the module is below. +// +// import init, { foo, bar } from "./my_module.mjs" +// await init(optionalArguments); +// foo(); +// bar(); +// // [link] var MODULARIZE = false; diff --git a/test/modularize_instance.c b/test/modularize_instance.c new file mode 100644 index 0000000000000..7a3270546e196 --- /dev/null +++ b/test/modularize_instance.c @@ -0,0 +1,35 @@ +#include +#include +#ifdef __EMSCRIPTEN_PTHREADS__ +#include +#include +#endif + +EMSCRIPTEN_KEEPALIVE void foo() { + printf("foo\n"); +} + +void bar() { + printf("bar\n"); +} + +void *thread_function(void *arg) { + printf("main2\n"); + return NULL; +} + +int main() { + printf("main1\n"); +#ifdef __EMSCRIPTEN_PTHREADS__ + pthread_t thread_id; + int result = pthread_create(&thread_id, NULL, thread_function, NULL); + if (result != 0) { + fprintf(stderr, "Error creating thread: %s\n", strerror(result)); + return 1; + } + pthread_join(thread_id, NULL); +#else + printf("main2\n"); +#endif + return 0; +} diff --git a/test/modularize_instance_runner.mjs b/test/modularize_instance_runner.mjs new file mode 100644 index 0000000000000..d52b4c4ec6be1 --- /dev/null +++ b/test/modularize_instance_runner.mjs @@ -0,0 +1,3 @@ +import init, { _foo as foo } from "./modularize_static.mjs"; +await init(); +foo(); diff --git a/test/test_other.py b/test/test_other.py index 594651b6cb33c..db997372b2d0a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -444,6 +444,37 @@ def test_export_es6(self, package_json, args): self.assertContained('hello, world!', self.run_js('runner.mjs')) + @parameterized({ + '': ([],), + 'pthreads': (['-pthread'],), + }) + def test_modularize_instance(self, args): + create_file('library.js', '''\ + addToLibrary({ + $baz: function() { console.log('baz'); }, + $qux: function() { console.log('qux'); } + });''') + self.run_process([EMCC, test_file('modularize_instance.c'), + '-sMODULARIZE=instance', + '-sEXPORTED_RUNTIME_METHODS=baz,addOnExit', + '-sEXPORTED_FUNCTIONS=_bar,_main,qux', + '--js-library', 'library.js', + '-o', 'modularize_instance.mjs'] + args) + + create_file('runner.mjs', ''' + import { strict as assert } from 'assert'; + import init, { _foo as foo, _bar as bar, baz, qux, addOnExit, HEAP32 } from "./modularize_instance.mjs"; + await init(); + foo(); // exported with EMSCRIPTEN_KEEPALIVE + bar(); // exported with EXPORTED_FUNCTIONS + baz(); // exported library function with EXPORTED_RUNTIME_METHODS + qux(); // exported library function with EXPORTED_FUNCTIONS + assert(typeof addOnExit === 'function'); // exported runtime function with EXPORTED_RUNTIME_METHODS + assert(typeof HEAP32 === 'object'); // exported runtime value by default + ''') + + self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs')) + def test_emcc_out_file(self): # Verify that "-ofile" works in addition to "-o" "file" self.run_process([EMCC, '-c', '-ofoo.o', test_file('hello_world.c')]) diff --git a/tools/emscripten.py b/tools/emscripten.py index 9e1bb6a226da8..f13391b5be020 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -912,8 +912,12 @@ def install_wrapper(sym): # TODO(sbc): Can we avoid exporting the dynCall_ functions on the module. should_export = settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS - if name.startswith('dynCall_') or should_export: - exported = "Module['%s'] = " % mangled + if (name.startswith('dynCall_') and settings.MODULARIZE != 'instance') or should_export: + if settings.MODULARIZE == 'instance': + # Update the export declared at the top level. + wrapper += f" __exp_{mangled} = " + else: + exported = "Module['%s'] = " % mangled else: exported = '' wrapper += exported diff --git a/tools/link.py b/tools/link.py index dbd4cb3852034..c554bf3e1292c 100644 --- a/tools/link.py +++ b/tools/link.py @@ -754,7 +754,15 @@ def phase_linker_setup(options, state, newargs): if options.oformat == OFormat.MJS: settings.EXPORT_ES6 = 1 - settings.MODULARIZE = 1 + default_setting('MODULARIZE', 1) + + if settings.MODULARIZE and settings.MODULARIZE not in [1, 'instance']: + exit_with_error(f'Invalid setting "{settings.MODULARIZE}" for MODULARIZE.') + + if settings.MODULARIZE == 'instance': + diagnostics.warning('experimental', '-sMODULARIZE=instance is still experimental. Many features may not work or will change.') + if options.oformat != OFormat.MJS: + exit_with_error('emcc: MODULARIZE instance is only compatible with .mjs output files') if options.oformat in (OFormat.WASM, OFormat.BARE): if options.emit_tsd: @@ -2391,7 +2399,20 @@ def modularize(): if async_emit != '' and settings.EXPORT_NAME == 'config': diagnostics.warning('emcc', 'EXPORT_NAME should not be named "config" when targeting Safari') - src = ''' + if settings.MODULARIZE == 'instance': + src = ''' +export default async function init(moduleArg = {}) { + var moduleRtn; + +%(src)s + + return await moduleRtn; +} +''' % { + 'src': src, + } + else: + src = ''' %(maybe_async)sfunction(moduleArg = {}) { var moduleRtn; @@ -2400,9 +2421,9 @@ def modularize(): return moduleRtn; } ''' % { - 'maybe_async': async_emit, - 'src': src, - } + 'maybe_async': async_emit, + 'src': src, + } if settings.MINIMAL_RUNTIME and not settings.PTHREADS: # Single threaded MINIMAL_RUNTIME programs do not need access to @@ -2421,19 +2442,31 @@ def modularize(): script_url = "typeof document != 'undefined' ? document.currentScript?.src : undefined" if shared.target_environment_may_be('node'): script_url_node = "if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;" - src = '''%(node_imports)s + if settings.MODULARIZE == 'instance': + src = '''%(node_imports)s + var _scriptName = %(script_url)s; + %(script_url_node)s + %(src)s +''' % { + 'node_imports': node_es6_imports(), + 'script_url': script_url, + 'script_url_node': script_url_node, + 'src': src, + } + else: + src = '''%(node_imports)s var %(EXPORT_NAME)s = (() => { var _scriptName = %(script_url)s; %(script_url_node)s return (%(src)s); })(); ''' % { - 'node_imports': node_es6_imports(), - 'EXPORT_NAME': settings.EXPORT_NAME, - 'script_url': script_url, - 'script_url_node': script_url_node, - 'src': src, - } + 'node_imports': node_es6_imports(), + 'EXPORT_NAME': settings.EXPORT_NAME, + 'script_url': script_url, + 'script_url_node': script_url_node, + 'src': src, + } # Given the async nature of how the Module function and Module object # come into existence in AudioWorkletGlobalScope, store the Module @@ -2446,8 +2479,16 @@ def modularize(): # Export using a UMD style export, or ES6 exports if selected if settings.EXPORT_ES6: - src += 'export default %s;\n' % settings.EXPORT_NAME - + if settings.MODULARIZE == 'instance': + exports = settings.EXPORTED_FUNCTIONS + settings.EXPORTED_RUNTIME_METHODS + # Declare a top level var for each export so that code in the init function + # can assign to it and update the live module bindings. + src += 'var ' + ', '.join(['__exp_' + export for export in exports]) + ';\n' + # Export the functions with their original name. + exports = ['__exp_' + export + ' as ' + export for export in exports] + src += 'export {' + ', '.join(exports) + '};\n' + else: + src += 'export default %s;\n' % settings.EXPORT_NAME elif not settings.MINIMAL_RUNTIME: src += '''\ if (typeof exports === 'object' && typeof module === 'object') @@ -2470,7 +2511,10 @@ def modularize(): elif settings.ENVIRONMENT_MAY_BE_NODE: src += f'var isPthread = {node_pthread_detection()}\n' src += '// When running as a pthread, construct a new instance on startup\n' - src += 'isPthread && %s();\n' % settings.EXPORT_NAME + if settings.MODULARIZE == 'instance': + src += 'isPthread && init();\n' + else: + src += 'isPthread && %s();\n' % settings.EXPORT_NAME final_js += '.modular.js' write_file(final_js, src) diff --git a/tools/settings.py b/tools/settings.py index fc884b3046deb..632c1d9e26ee5 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -267,7 +267,8 @@ def __setattr__(self, name, value): self.attrs[name] = value def check_type(self, name, value): - if name in ('SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO'): + # These settings have a variable type so cannot be easily type checked. + if name in ('SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE'): return expected_type = self.types.get(name) if not expected_type: