Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 16 additions & 0 deletions site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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 === 'instance') {
return `__exp_${name} = ${name};`;
}
return `Module['${name}'] = ${name};`;
}
}
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 === 'instance') {
return `__exp_${x} = `
}
return `Module['${x}'] = `;
}
return '';
};
null;
}}}
Expand Down
17 changes: 17 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
35 changes: 35 additions & 0 deletions test/modularize_instance.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <stdio.h>
#include <emscripten.h>
#ifdef __EMSCRIPTEN_PTHREADS__
#include <pthread.h>
#include <string.h>
#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;
}
3 changes: 3 additions & 0 deletions test/modularize_instance_runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import init, { _foo as foo } from "./modularize_static.mjs";
await init();
foo();
31 changes: 31 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')])
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 != '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
Expand Down
74 changes: 59 additions & 15 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;

Expand All @@ -2400,9 +2421,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 @@ -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
Expand All @@ -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')
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion tools/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading