From 5c0d413163938c32757dca8cbabda387c96856f0 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 6 Nov 2024 18:40:52 +0000 Subject: [PATCH 1/7] 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. --- .../tools_reference/settings_reference.rst | 18 ++++- src/jsifier.mjs | 1 + src/modules.mjs | 3 + src/postamble_modularize.js | 2 +- src/runtime_shared.js | 9 ++- src/settings.js | 19 ++++- test/test_other.py | 29 ++++++++ tools/emscripten.py | 8 ++- tools/link.py | 71 +++++++++++++++---- tools/settings.py | 2 +- 10 files changed, 140 insertions(+), 22 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index dfc4583a879c1..f066023d3fa3c 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1947,7 +1947,23 @@ 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. -Default value: false +Experimental Feature - Static ES Modules: + +Note this feature is still under active development and is subject to change! + +To enable this feature use -sMODULARIZE=static. Enabling this mode will +produce an ES module that is a singleton with static 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: '' .. _export_es6: diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 233928161ccd3..3c7829be82650 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -644,6 +644,7 @@ 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) { + assert(MODULARIZE !== 'static', 'Exports in jsifier not currently supported with static modules.'); contentText += `\nModule['${mangled}'] = ${mangled};`; } // Relocatable code needs signatures to create proper wrappers. diff --git a/src/modules.mjs b/src/modules.mjs index eea19009a01d5..d215f67e77bc5 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 === 'static') { + return `x_${name} = ${name};`; + } return `Module['${name}'] = ${name};`; } } diff --git a/src/postamble_modularize.js b/src/postamble_modularize.js index d8d3a59a4671e..393e8486c56b1 100644 --- a/src/postamble_modularize.js +++ b/src/postamble_modularize.js @@ -18,7 +18,7 @@ moduleRtn = Module; #endif // WASM_ASYNC_COMPILATION -#if ASSERTIONS +#if ASSERTIONS && MODULARIZE != 'static' // Assertion for attempting to access module properties on the incoming // moduleArg. In the past we used this object as the prototype of the module // and assigned properties to it, but now we return a distinct object. This diff --git a/src/runtime_shared.js b/src/runtime_shared.js index 9a9943c3ad13e..7da059d0108e3 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 === 'static') { + return `x_${x} = ` + } + return `Module['${x}'] = `; + } + return ''; }; null; }}} diff --git a/src/settings.js b/src/settings.js index 981c44fa3b48a..34c71070bfcd4 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1325,8 +1325,25 @@ 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 - Static ES Modules: +// +// Note this feature is still under active development and is subject to change! +// +// To enable this feature use -sMODULARIZE=static. Enabling this mode will +// produce an ES module that is a singleton with static 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; +var MODULARIZE = ''; // Export using an ES6 Module export rather than a UMD export. MODULARIZE must // be enabled for ES6 exports and is implicitly enabled if not already set. diff --git a/test/test_other.py b/test/test_other.py index 7ab83b66589c3..137ffb0c8fb57 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -442,6 +442,35 @@ def test_export_es6(self, package_json, args): self.assertContained('hello, world!', self.run_js('runner.mjs')) + @parameterized({ + '': ([],), + 'pthreads': (['-pthread'],), + }) + def test_modularize_static(self, args): + create_file('library.js', '''\ + addToLibrary({ + $baz: function() { console.log('baz'); } + });''') + self.run_process([EMCC, test_file('modularize_static.cpp'), + '-sMODULARIZE=static', + '-sEXPORTED_RUNTIME_METHODS=baz,addOnExit', + '-sEXPORTED_FUNCTIONS=_bar,_main', + '--js-library', 'library.js', + '-o', 'modularize_static.mjs'] + args) + + create_file('runner.mjs', ''' + import { strict as assert } from 'assert'; + import init, { _foo as foo, _bar as bar, baz, addOnExit, HEAP32 } from "./modularize_static.mjs"; + await init(); + foo(); // exported with EMSCRIPTEN_KEEPALIVE + bar(); // exported with EXPORTED_FUNCTIONS + baz(); // exported library function with EXPORTED_RUNTIME_METHODS + 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..d05b4b77a0dd2 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 != 'static') or should_export: + if settings.MODULARIZE == 'static': + # Update the export declared at the top level. + wrapper += f" x_{mangled} = " + else: + exported = "Module['%s'] = " % mangled else: exported = '' wrapper += exported diff --git a/tools/link.py b/tools/link.py index b7759ff0b0b24..80601fb6bd43b 100644 --- a/tools/link.py +++ b/tools/link.py @@ -757,7 +757,13 @@ def phase_linker_setup(options, state, newargs): if options.oformat == OFormat.MJS: settings.EXPORT_ES6 = 1 - settings.MODULARIZE = 1 + if not settings.MODULARIZE: + settings.MODULARIZE = 1 + + if settings.MODULARIZE == 'static': + diagnostics.warning('experimental', '-sMODULARIZE=static is still experimental. Many features may not work or will change.') + if options.oformat != OFormat.MJS: + exit_with_error('emcc: MODULARIZE static is only compatible with .mjs output files') if options.oformat in (OFormat.WASM, OFormat.BARE): if options.emit_tsd: @@ -2394,7 +2400,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 == 'static': + src = ''' +export default async function init(moduleArg = {}) { + var moduleRtn; + +%(src)s + + return await moduleRtn; +} +''' % { + 'src': src, + } + else: + src = ''' %(maybe_async)sfunction(moduleArg = {}) { var moduleRtn; @@ -2403,9 +2422,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 @@ -2424,19 +2443,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 == 'static': + 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 @@ -2448,9 +2479,18 @@ def modularize(): src += f'globalThis.AudioWorkletModule = {settings.EXPORT_NAME};\n' # Export using a UMD style export, or ES6 exports if selected - if settings.EXPORT_ES6: + if settings.EXPORT_ES6 and settings.MODULARIZE != 'static': src += 'export default %s;\n' % settings.EXPORT_NAME + if settings.MODULARIZE == 'static': + 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(['x_' + export for export in exports]) + ";\n" + # Export the functions with their original name. + exports = ['x_' + export + ' as ' + export for export in exports] + src += "export {" + ", ".join(exports) + "};\n" + elif not settings.MINIMAL_RUNTIME: src += '''\ if (typeof exports === 'object' && typeof module === 'object') @@ -2473,7 +2513,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 == 'static': + 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..442e92ac55e98 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -267,7 +267,7 @@ 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'): + if name in ('SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE'): return expected_type = self.types.get(name) if not expected_type: From b9e19a0c217bf7627e264b70f7350131d7bab0e4 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 6 Nov 2024 22:11:42 +0000 Subject: [PATCH 2/7] Review comments and missing test. --- src/jsifier.mjs | 5 ++++- src/modules.mjs | 2 +- src/runtime_shared.js | 2 +- test/modularize_static.cpp | 24 ++++++++++++++++++++++++ test/modularize_static_runner.mjs | 3 +++ tools/emscripten.py | 2 +- tools/link.py | 9 ++++----- tools/settings.py | 1 + 8 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 test/modularize_static.cpp create mode 100644 test/modularize_static_runner.mjs diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 3c7829be82650..593eb894c956c 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -644,7 +644,10 @@ 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) { - assert(MODULARIZE !== 'static', 'Exports in jsifier not currently supported with static modules.'); + assert( + MODULARIZE !== 'static', + 'Exports in jsifier not currently supported with static modules.', + ); contentText += `\nModule['${mangled}'] = ${mangled};`; } // Relocatable code needs signatures to create proper wrappers. diff --git a/src/modules.mjs b/src/modules.mjs index d215f67e77bc5..d35bb13e8aaca 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -385,7 +385,7 @@ function exportRuntime() { // separately in updateMemoryViews if (EXPORTED_RUNTIME_METHODS.has(name) && !name.startsWith('HEAP')) { if (MODULARIZE === 'static') { - return `x_${name} = ${name};`; + return `__exp_${name} = ${name};`; } return `Module['${name}'] = ${name};`; } diff --git a/src/runtime_shared.js b/src/runtime_shared.js index 7da059d0108e3..106911643f00d 100644 --- a/src/runtime_shared.js +++ b/src/runtime_shared.js @@ -24,7 +24,7 @@ } if (shouldExport) { if (MODULARIZE === 'static') { - return `x_${x} = ` + return `__exp_${x} = ` } return `Module['${x}'] = `; } diff --git a/test/modularize_static.cpp b/test/modularize_static.cpp new file mode 100644 index 0000000000000..d11571bf43584 --- /dev/null +++ b/test/modularize_static.cpp @@ -0,0 +1,24 @@ +#include +#include +#ifdef __EMSCRIPTEN_PTHREADS__ +#include +#endif + +extern "C" EMSCRIPTEN_KEEPALIVE void foo() { + printf("foo\n"); +} + +extern "C" void bar() { + printf("bar\n"); +} + +int main() { + printf("main1\n"); +#ifdef __EMSCRIPTEN_PTHREADS__ + std::thread([]{ + printf("main2\n"); + }).join(); +#else + printf("main2\n"); +#endif +} diff --git a/test/modularize_static_runner.mjs b/test/modularize_static_runner.mjs new file mode 100644 index 0000000000000..81e1e5af0c9ed --- /dev/null +++ b/test/modularize_static_runner.mjs @@ -0,0 +1,3 @@ +import init, { _foo as foo } from "./modularize_static.mjs"; +await init(); +foo(); \ No newline at end of file diff --git a/tools/emscripten.py b/tools/emscripten.py index d05b4b77a0dd2..364b275c5e15c 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -915,7 +915,7 @@ def install_wrapper(sym): if (name.startswith('dynCall_') and settings.MODULARIZE != 'static') or should_export: if settings.MODULARIZE == 'static': # Update the export declared at the top level. - wrapper += f" x_{mangled} = " + wrapper += f" __exp_{mangled} = " else: exported = "Module['%s'] = " % mangled else: diff --git a/tools/link.py b/tools/link.py index 80601fb6bd43b..3da4d7f291dda 100644 --- a/tools/link.py +++ b/tools/link.py @@ -757,8 +757,7 @@ def phase_linker_setup(options, state, newargs): if options.oformat == OFormat.MJS: settings.EXPORT_ES6 = 1 - if not settings.MODULARIZE: - settings.MODULARIZE = 1 + default_setting('MODULARIZE', 1) if settings.MODULARIZE == 'static': diagnostics.warning('experimental', '-sMODULARIZE=static is still experimental. Many features may not work or will change.') @@ -2486,10 +2485,10 @@ def modularize(): 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(['x_' + export for export in exports]) + ";\n" + src += 'var ' + ', '.join(['__exp_' + export for export in exports]) + ';\n' # Export the functions with their original name. - exports = ['x_' + export + ' as ' + export for export in exports] - src += "export {" + ", ".join(exports) + "};\n" + exports = ['__exp_' + export + ' as ' + export for export in exports] + src += 'export {' + ', '.join(exports) + '};\n' elif not settings.MINIMAL_RUNTIME: src += '''\ diff --git a/tools/settings.py b/tools/settings.py index 442e92ac55e98..632c1d9e26ee5 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -267,6 +267,7 @@ def __setattr__(self, name, value): self.attrs[name] = value def check_type(self, name, value): + # 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) From f6b40eefd354621eb5c29f9a5215ffd71ecfb986 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Fri, 8 Nov 2024 23:02:30 +0000 Subject: [PATCH 3/7] Support exporting library functions with EXPORTED_FUNCTIONS. --- src/jsifier.mjs | 10 +++++----- test/test_other.py | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 593eb894c956c..925500b861c11 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -644,11 +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) { - assert( - MODULARIZE !== 'static', - 'Exports in jsifier not currently supported with static modules.', - ); - contentText += `\nModule['${mangled}'] = ${mangled};`; + if (MODULARIZE === 'static') { + contentText += `\n__exp_${mangled} = ${mangled};`; + } else { + contentText += `\nModule['${mangled}'] = ${mangled};`; + } } // Relocatable code needs signatures to create proper wrappers. if (sig && RELOCATABLE) { diff --git a/test/test_other.py b/test/test_other.py index 137ffb0c8fb57..f9ca7d57e6ced 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -449,22 +449,24 @@ def test_export_es6(self, package_json, args): def test_modularize_static(self, args): create_file('library.js', '''\ addToLibrary({ - $baz: function() { console.log('baz'); } + $baz: function() { console.log('baz'); }, + $qux: function() { console.log('qux'); } });''') self.run_process([EMCC, test_file('modularize_static.cpp'), '-sMODULARIZE=static', '-sEXPORTED_RUNTIME_METHODS=baz,addOnExit', - '-sEXPORTED_FUNCTIONS=_bar,_main', + '-sEXPORTED_FUNCTIONS=_bar,_main,qux', '--js-library', 'library.js', '-o', 'modularize_static.mjs'] + args) create_file('runner.mjs', ''' import { strict as assert } from 'assert'; - import init, { _foo as foo, _bar as bar, baz, addOnExit, HEAP32 } from "./modularize_static.mjs"; + import init, { _foo as foo, _bar as bar, baz, qux, addOnExit, HEAP32 } from "./modularize_static.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 ''') From ecf9e4f78ee983e24cd50c5eb7a5a0088de9b77e Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 12 Nov 2024 19:34:01 +0000 Subject: [PATCH 4/7] Change name to instance. Add check for correct setting name. --- .../tools_reference/settings_reference.rst | 6 +++--- src/jsifier.mjs | 2 +- src/modules.mjs | 2 +- src/postamble_modularize.js | 2 +- src/runtime_shared.js | 2 +- src/settings.js | 6 +++--- ...ize_static.cpp => modularize_instance.cpp} | 0 ...ner.mjs => modularize_instance_runner.mjs} | 0 test/test_other.py | 10 +++++----- tools/emscripten.py | 4 ++-- tools/link.py | 19 +++++++++++-------- 11 files changed, 28 insertions(+), 25 deletions(-) rename test/{modularize_static.cpp => modularize_instance.cpp} (100%) rename test/{modularize_static_runner.mjs => modularize_instance_runner.mjs} (100%) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index f066023d3fa3c..c4a8858123820 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1947,12 +1947,12 @@ 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 - Static ES Modules: +Experimental Feature - Instance ES Modules: Note this feature is still under active development and is subject to change! -To enable this feature use -sMODULARIZE=static. Enabling this mode will -produce an ES module that is a singleton with static ES module exports. The +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 diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 925500b861c11..b9a4ae1a5f4e0 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -644,7 +644,7 @@ 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) { - if (MODULARIZE === 'static') { + if (MODULARIZE === 'instance') { contentText += `\n__exp_${mangled} = ${mangled};`; } else { contentText += `\nModule['${mangled}'] = ${mangled};`; diff --git a/src/modules.mjs b/src/modules.mjs index d35bb13e8aaca..bed0931d88ef6 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -384,7 +384,7 @@ 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 === 'static') { + if (MODULARIZE === 'instance') { return `__exp_${name} = ${name};`; } return `Module['${name}'] = ${name};`; diff --git a/src/postamble_modularize.js b/src/postamble_modularize.js index 393e8486c56b1..4fb09f67f939a 100644 --- a/src/postamble_modularize.js +++ b/src/postamble_modularize.js @@ -18,7 +18,7 @@ moduleRtn = Module; #endif // WASM_ASYNC_COMPILATION -#if ASSERTIONS && MODULARIZE != 'static' +#if ASSERTIONS && MODULARIZE != 'instance' // Assertion for attempting to access module properties on the incoming // moduleArg. In the past we used this object as the prototype of the module // and assigned properties to it, but now we return a distinct object. This diff --git a/src/runtime_shared.js b/src/runtime_shared.js index 106911643f00d..22ef5e881c426 100644 --- a/src/runtime_shared.js +++ b/src/runtime_shared.js @@ -23,7 +23,7 @@ } } if (shouldExport) { - if (MODULARIZE === 'static') { + if (MODULARIZE === 'instance') { return `__exp_${x} = ` } return `Module['${x}'] = `; diff --git a/src/settings.js b/src/settings.js index 34c71070bfcd4..7fef7746dafe9 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1326,12 +1326,12 @@ var DETERMINISTIC = false; // 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 - Static ES Modules: +// Experimental Feature - Instance ES Modules: // // Note this feature is still under active development and is subject to change! // -// To enable this feature use -sMODULARIZE=static. Enabling this mode will -// produce an ES module that is a singleton with static ES module exports. The +// 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 diff --git a/test/modularize_static.cpp b/test/modularize_instance.cpp similarity index 100% rename from test/modularize_static.cpp rename to test/modularize_instance.cpp diff --git a/test/modularize_static_runner.mjs b/test/modularize_instance_runner.mjs similarity index 100% rename from test/modularize_static_runner.mjs rename to test/modularize_instance_runner.mjs diff --git a/test/test_other.py b/test/test_other.py index f9ca7d57e6ced..2f25a6d0e8d5e 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -446,22 +446,22 @@ def test_export_es6(self, package_json, args): '': ([],), 'pthreads': (['-pthread'],), }) - def test_modularize_static(self, args): + 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_static.cpp'), - '-sMODULARIZE=static', + self.run_process([EMCC, test_file('modularize_instance.cpp'), + '-sMODULARIZE=instance', '-sEXPORTED_RUNTIME_METHODS=baz,addOnExit', '-sEXPORTED_FUNCTIONS=_bar,_main,qux', '--js-library', 'library.js', - '-o', 'modularize_static.mjs'] + args) + '-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_static.mjs"; + 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 diff --git a/tools/emscripten.py b/tools/emscripten.py index 364b275c5e15c..f13391b5be020 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -912,8 +912,8 @@ 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_') and settings.MODULARIZE != 'static') or should_export: - if settings.MODULARIZE == 'static': + 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: diff --git a/tools/link.py b/tools/link.py index 3da4d7f291dda..69837e9fc9d9b 100644 --- a/tools/link.py +++ b/tools/link.py @@ -759,10 +759,13 @@ def phase_linker_setup(options, state, newargs): settings.EXPORT_ES6 = 1 default_setting('MODULARIZE', 1) - if settings.MODULARIZE == 'static': - diagnostics.warning('experimental', '-sMODULARIZE=static is still experimental. Many features may not work or will change.') + 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 static is only compatible with .mjs output files') + 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: @@ -2399,7 +2402,7 @@ def modularize(): if async_emit != '' and settings.EXPORT_NAME == 'config': diagnostics.warning('emcc', 'EXPORT_NAME should not be named "config" when targeting Safari') - if settings.MODULARIZE == 'static': + if settings.MODULARIZE == 'instance': src = ''' export default async function init(moduleArg = {}) { var moduleRtn; @@ -2442,7 +2445,7 @@ 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;" - if settings.MODULARIZE == 'static': + if settings.MODULARIZE == 'instance': src = '''%(node_imports)s var _scriptName = %(script_url)s; %(script_url_node)s @@ -2478,10 +2481,10 @@ def modularize(): src += f'globalThis.AudioWorkletModule = {settings.EXPORT_NAME};\n' # Export using a UMD style export, or ES6 exports if selected - if settings.EXPORT_ES6 and settings.MODULARIZE != 'static': + if settings.EXPORT_ES6 and settings.MODULARIZE != 'instance': src += 'export default %s;\n' % settings.EXPORT_NAME - if settings.MODULARIZE == 'static': + 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. @@ -2512,7 +2515,7 @@ 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' - if settings.MODULARIZE == 'static': + if settings.MODULARIZE == 'instance': src += 'isPthread && init();\n' else: src += 'isPthread && %s();\n' % settings.EXPORT_NAME From dcc5a8aa38d7646479951e59f6fd72bdbb923c05 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 19 Nov 2024 19:11:02 +0000 Subject: [PATCH 5/7] Review comments. --- src/postamble_modularize.js | 2 +- test/modularize_instance.c | 35 +++++++++++++++++++++++++++++ test/modularize_instance.cpp | 24 -------------------- test/modularize_instance_runner.mjs | 2 +- test/test_other.py | 2 +- tools/link.py | 23 +++++++++---------- 6 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 test/modularize_instance.c delete mode 100644 test/modularize_instance.cpp diff --git a/src/postamble_modularize.js b/src/postamble_modularize.js index 4fb09f67f939a..d8d3a59a4671e 100644 --- a/src/postamble_modularize.js +++ b/src/postamble_modularize.js @@ -18,7 +18,7 @@ moduleRtn = Module; #endif // WASM_ASYNC_COMPILATION -#if ASSERTIONS && MODULARIZE != 'instance' +#if ASSERTIONS // Assertion for attempting to access module properties on the incoming // moduleArg. In the past we used this object as the prototype of the module // and assigned properties to it, but now we return a distinct object. This 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.cpp b/test/modularize_instance.cpp deleted file mode 100644 index d11571bf43584..0000000000000 --- a/test/modularize_instance.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include -#include -#ifdef __EMSCRIPTEN_PTHREADS__ -#include -#endif - -extern "C" EMSCRIPTEN_KEEPALIVE void foo() { - printf("foo\n"); -} - -extern "C" void bar() { - printf("bar\n"); -} - -int main() { - printf("main1\n"); -#ifdef __EMSCRIPTEN_PTHREADS__ - std::thread([]{ - printf("main2\n"); - }).join(); -#else - printf("main2\n"); -#endif -} diff --git a/test/modularize_instance_runner.mjs b/test/modularize_instance_runner.mjs index 81e1e5af0c9ed..d52b4c4ec6be1 100644 --- a/test/modularize_instance_runner.mjs +++ b/test/modularize_instance_runner.mjs @@ -1,3 +1,3 @@ import init, { _foo as foo } from "./modularize_static.mjs"; await init(); -foo(); \ No newline at end of file +foo(); diff --git a/test/test_other.py b/test/test_other.py index 8c4075d6c3277..68513cb3e23f8 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -454,7 +454,7 @@ def test_modularize_instance(self, args): $baz: function() { console.log('baz'); }, $qux: function() { console.log('qux'); } });''') - self.run_process([EMCC, test_file('modularize_instance.cpp'), + self.run_process([EMCC, test_file('modularize_instance.c'), '-sMODULARIZE=instance', '-sEXPORTED_RUNTIME_METHODS=baz,addOnExit', '-sEXPORTED_FUNCTIONS=_bar,_main,qux', diff --git a/tools/link.py b/tools/link.py index 69837e9fc9d9b..53e04961e1533 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2481,18 +2481,17 @@ def modularize(): src += f'globalThis.AudioWorkletModule = {settings.EXPORT_NAME};\n' # Export using a UMD style export, or ES6 exports if selected - if settings.EXPORT_ES6 and settings.MODULARIZE != 'instance': - 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' - + if settings.EXPORT_ES6: + 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') From 694fb8a9afc809bc7279d7bfb7c38c8116a9342f Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 19 Nov 2024 19:59:31 +0000 Subject: [PATCH 6/7] comment. --- src/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.js b/src/settings.js index 55c2f41d2a45f..e009a98c907e7 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1343,7 +1343,7 @@ var DETERMINISTIC = false; // bar(); // // [link] -var MODULARIZE = ''; +var MODULARIZE = false; // Export using an ES6 Module export rather than a UMD export. MODULARIZE must // be enabled for ES6 exports and is implicitly enabled if not already set. From a7d28ef49d653e1a129f5ed57432718ebe00ac75 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 19 Nov 2024 22:42:39 +0000 Subject: [PATCH 7/7] docs --- site/source/docs/tools_reference/settings_reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 1bc2f4a1d2119..b8a971f917e71 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1963,7 +1963,7 @@ used. An example of using the module is below. foo(); bar(); -Default value: '' +Default value: false .. _export_es6: