Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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};`;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/postamble_modularize.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ moduleRtn = Module;

#endif // WASM_ASYNC_COMPILATION

#if ASSERTIONS
#if ASSERTIONS && MODULARIZE != 'static'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use -sMODULARIZE=instance to match the old -sMODULARIZE_INSTANCE setting? Do you think the word "static" is more descriptive of what is happening here and "instance"? I think of the two modes as "single instance" vs "factory/multiple instance", but maybe "static" makes sense on some level?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I open to changing, but instance to me confusing. When I hear instance, it makes me think I can create multiple instances i.e. a factory. I chose static since the exports to the outside world don't change and there's only one static instance.

Other ideas: static_instance, singleton, esmodule. Or alternatively, we add a whole different flag that will enable all the settings we want in the "new output world" e.g. strict, es modules, no _ exports....

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kind of leaning towards bring back the old name -sMODULARIZE_INSTANCE until we can think of a better one. It is exactly the same meaning as the old option I believe, right?

Its interesting to me that the word instance to you implies multiple instances. I don't think it has the connotation for me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed it instance for now. Later I'd like to look into the idea of having two independent options of FACTORY(aka MODULARIZE) and then MODULE_FORMAT = none/esm/umd/... as mentioned below and in the other bug.

// 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
Expand Down
9 changes: 7 additions & 2 deletions src/runtime_shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@
shouldExport = true;
}
}

return shouldExport ? `Module['${x}'] = ` : '';
if (shouldExport) {
if (MODULARIZE === 'static') {
return `x_${x} = `
}
return `Module['${x}'] = `;
}
return '';
};
null;
}}}
Expand Down
19 changes: 18 additions & 1 deletion src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you leave this as "false" and instead add this to the list of settings we cannot type check in tools/settings.py in check_type?


// 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.
Expand Down
29 changes: 29 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')])
Expand Down
8 changes: 6 additions & 2 deletions tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 57 additions & 14 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;

Expand All @@ -2403,9 +2422,9 @@ def modularize():
return moduleRtn;
}
''' % {
'maybe_async': async_emit,
'src': src,
}
'maybe_async': async_emit,
'src': src,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this indentation better than before? Maybe revert this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flake wants this because of the new if/else above.


if settings.MINIMAL_RUNTIME and not settings.PTHREADS:
# Single threaded MINIMAL_RUNTIME programs do not need access to
Expand All @@ -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
Expand All @@ -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')
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tools/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading