Skip to content

Commit 423a77f

Browse files
author
Louis DeScioli
authored
Implements Promise-based API for modularized builds (#10697)
This removes the Module.then function and replaces it with a proper Promise being returned from the initialization function. That is, when in MODULARIZE mode, calling `Module()` does not return an instance, but instead returns a Promise you can wait on for when it is ready. Fixes #5820
1 parent 94ed05f commit 423a77f

21 files changed

+190
-186
lines changed

ChangeLog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Current Trunk
2424

2525
1.39.15: 05/06/2020
2626
-------------------
27+
- Change the factory function created by using the `MODULARIZE` build option to
28+
return a Promise instead of the module instance. If you use `MODULARIZE` you
29+
will need to wait on the returned Promise, using `await` or its `then`
30+
callback, to get the module instance (#10697).
2731
- Add `--extern-pre-js` and `--extern-post-js` emcc flags. Files provided there
2832
are prepended/appended to the final JavaScript output, *after* all other
2933
work has been done, including optimization, optional `MODULARIZE`-ation,

emcc.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,9 +1320,6 @@ def has_c_source(args):
13201320

13211321
if shared.Settings.MODULARIZE:
13221322
assert not options.proxy_to_worker, '-s MODULARIZE=1 is not compatible with --proxy-to-worker (if you want to run in a worker with -s MODULARIZE=1, you likely want to do the worker side setup manually)'
1323-
# MODULARIZE's .then() method uses onRuntimeInitialized currently, so make sure
1324-
# it is expected to be used.
1325-
shared.Settings.INCOMING_MODULE_JS_API += ['onRuntimeInitialized']
13261323

13271324
if shared.Settings.EMULATE_FUNCTION_POINTER_CASTS:
13281325
shared.Settings.ALIASING_FUNCTION_POINTERS = 0
@@ -3404,22 +3401,17 @@ def modularize():
34043401
logger.debug('Modularizing, assigning to var ' + shared.Settings.EXPORT_NAME)
34053402
src = open(final).read()
34063403

3407-
# TODO: exports object generation for MINIMAL_RUNTIME. As a temp measure, multithreaded MINIMAL_RUNTIME builds export like regular
3408-
# runtime does, so that worker.js can see the JS module contents.
3409-
exports_object = '{}' if shared.Settings.MINIMAL_RUNTIME and not shared.Settings.USE_PTHREADS else shared.Settings.EXPORT_NAME
3410-
34113404
src = '''
34123405
function(%(EXPORT_NAME)s) {
34133406
%(EXPORT_NAME)s = %(EXPORT_NAME)s || {};
34143407
34153408
%(src)s
34163409
3417-
return %(exports_object)s
3410+
return %(EXPORT_NAME)s.ready;
34183411
}
34193412
''' % {
34203413
'EXPORT_NAME': shared.Settings.EXPORT_NAME,
34213414
'src': src,
3422-
'exports_object': exports_object
34233415
}
34243416

34253417
if shared.Settings.MINIMAL_RUNTIME and not shared.Settings.USE_PTHREADS:

site/source/docs/getting_started/FAQ.rst

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -262,24 +262,13 @@ Here is an example of how to use it:
262262

263263
The crucial thing is that ``Module`` exists, and has the property ``onRuntimeInitialized``, before the script containing emscripten output (``my_project.js`` in this example) is loaded.
264264

265-
Another option is to use the ``MODULARIZE`` option, using ``-s MODULARIZE=1``. That will put all of the generated JavaScript in a function, which you can call to create an instance. The instance has a promise-like `.then()` method, so if you build with say ``-s MODULARIZE=1 -s 'EXPORT_NAME="MyCode"'`` (see details in settings.js), then you can do something like this:
265+
Another option is to use the ``MODULARIZE`` option, using ``-s MODULARIZE=1``. That puts all of the generated JavaScript into a factory function, which you can call to create an instance of your module. The factory function returns a Promise that resolves with the module instance. The promise is resolved once it's safe to call the compiled code, i.e. after the compiled code has been downloaded and instantiated. For example, if you build with ``-s MODULARIZE=1 -s 'EXPORT_NAME="createMyModule"'`` (see details in settings.js), then you can do this:
266266

267267
::
268268

269-
MyCode().then(function(Module) {
270-
// this is reached when everything is ready, and you can call methods on Module
269+
createMyModule().then((myModule) => {
270+
// this is reached when everything is ready, and you can call methods on myModule
271271
});
272-
273-
.. note:: Be careful with using ``Module.then()`` inside ``Promise.resolve()`` or ``await`` statements. ``Module.then()`` resolves with ``Module`` which still has ``then``, so the ``await`` statement loops indefinitely. If you need to use ``Module.then()`` with ``await`` statement, you can use this workaround:
274-
275-
::
276-
277-
await Module.then(m => {
278-
delete m['then'];
279-
resolve(m);
280-
});
281-
282-
For more information read `this issue <https://github.com/emscripten-core/emscripten/pull/10697>`_.
283272

284273
.. _faq-NO_EXIT_RUNTIME:
285274

site/source/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.rst

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -113,28 +113,23 @@ Modular output
113113

114114
When using the WebIDL binder, often what you are doing is creating a library. In that
115115
case, the `MODULARIZE` option makes sense to use. It wraps the entire JavaScript output
116-
in a function, which you call to create instances,
116+
in a function, and returns a Promise which resolves to the initialized Module instance.
117117

118118
.. code-block:: javascript
119119
120-
var instance = Module();
121-
122-
(You can use the `EXPORT_NAME` option to change `Module` to something else.) This is
123-
good practice for libraries, as then they don't include unnecessary things in the
124-
global scope (and in some cases you want to create more than one).
125-
126-
Modularize also provides a promise-like API, which is the recommended way to use it,
127-
128-
.. code-block:: javascript
129-
130-
var instance = Module().then(function(instance) {
131-
// code that uses the instance here
120+
var instance;
121+
Module().then(module => {
122+
instance = module;
132123
});
133124
134-
The callback is called when it is safe to run compiled code, similar
135-
to the `onRuntimeInitialized` callback (i.e., it waits for all
136-
necessary async events). It receives the instance as a parameter,
137-
for convenience.
125+
The promise is resolved when it is safe to run compiled code, i.e. after it
126+
has been has been downloaded and instantiated. The promise is resolved at the
127+
same time the `onRuntimeInitialized` callback is invoked, so there's no need to
128+
use `onRuntimeInitialized` when using `MODULARIZE`.
129+
130+
You can use the `EXPORT_NAME` option to change `Module` to something else. This is
131+
good practice for libraries, as then they don't include unnecessary things in the
132+
global scope, and in some cases you want to create more than one.
138133

139134

140135
Using C++ classes in JavaScript

src/closure-externs/closure-externs.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,7 +999,6 @@ var noExitRuntime;
999999
// https://github.com/google/closure-compiler/issues/3167
10001000
var BigInt;
10011001

1002-
10031002
// Worklet
10041003
/**
10051004
* @constructor

src/postamble.js

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ if (memoryInitializer) {
6868
};
6969
var doBrowserLoad = function() {
7070
readAsync(memoryInitializer, applyMemoryInitializer, function() {
71-
throw 'could not load memory initializer ' + memoryInitializer;
71+
var e = new Error('could not load memory initializer ' + memoryInitializer);
72+
#if MODULARIZE
73+
readyPromiseReject(e);
74+
#else
75+
throw e;
76+
#endif
7277
});
7378
};
7479
#if SUPPORT_BASE64_EMBEDDING
@@ -121,34 +126,6 @@ if (memoryInitializer) {
121126

122127
var calledRun;
123128

124-
#if MODULARIZE
125-
// Modularize mode returns a function, which can be called to
126-
// create instances. The instances provide a then() method,
127-
// must like a Promise, that receives a callback. The callback
128-
// is called when the module is ready to run, with the module
129-
// as a parameter. (Like a Promise, it also returns the module
130-
// so you can use the output of .then(..)).
131-
Module['then'] = function(func) {
132-
// We may already be ready to run code at this time. if
133-
// so, just queue a call to the callback.
134-
if (calledRun) {
135-
func(Module);
136-
} else {
137-
// we are not ready to call then() yet. we must call it
138-
// at the same time we would call onRuntimeInitialized.
139-
#if ASSERTIONS && !expectToReceiveOnModule('onRuntimeInitialized')
140-
abort('.then() requires adding onRuntimeInitialized to INCOMING_MODULE_JS_API');
141-
#endif
142-
var old = Module['onRuntimeInitialized'];
143-
Module['onRuntimeInitialized'] = function() {
144-
if (old) old();
145-
func(Module);
146-
};
147-
}
148-
return Module;
149-
};
150-
#endif
151-
152129
/**
153130
* @constructor
154131
* @this {ExitStatus}
@@ -317,6 +294,9 @@ function run(args) {
317294

318295
preMain();
319296

297+
#if MODULARIZE
298+
readyPromiseResolve(Module);
299+
#endif
320300
#if expectToReceiveOnModule('onRuntimeInitialized')
321301
if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized']();
322302
#endif
@@ -427,9 +407,17 @@ function exit(status, implicit) {
427407
// if exit() was called, we may warn the user if the runtime isn't actually being shut down
428408
if (!implicit) {
429409
#if EXIT_RUNTIME == 0
430-
err('program exited (with status: ' + status + '), but EXIT_RUNTIME is not set, so halting execution but not exiting the runtime or preventing further async execution (build with EXIT_RUNTIME=1, if you want a true shutdown)');
410+
var msg = 'program exited (with status: ' + status + '), but EXIT_RUNTIME is not set, so halting execution but not exiting the runtime or preventing further async execution (build with EXIT_RUNTIME=1, if you want a true shutdown)';
411+
err(msg);
412+
#if MODULARIZE
413+
readyPromiseReject(msg);
414+
#endif // MODULARIZE
431415
#else
432-
err('program exited (with status: ' + status + '), but noExitRuntime is set due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)');
416+
var msg = 'program exited (with status: ' + status + '), but noExitRuntime is set due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)';
417+
err(msg);
418+
#if MODULARIZE
419+
readyPromiseReject(msg);
420+
#endif // MODULARIZE
433421
#endif // EXIT_RUNTIME
434422
}
435423
#endif // ASSERTIONS
@@ -482,15 +470,24 @@ if (!ENVIRONMENT_IS_PTHREAD) // EXIT_RUNTIME=0 only applies to default behavior
482470
#endif
483471

484472
#if USE_PTHREADS
485-
if (!ENVIRONMENT_IS_PTHREAD) run();
473+
if (!ENVIRONMENT_IS_PTHREAD) {
474+
run();
475+
} else {
486476
#if EMBIND
487-
else { // Embind must initialize itself on all threads, as it generates support JS.
477+
// Embind must initialize itself on all threads, as it generates support JS.
488478
Module['___embind_register_native_and_builtin_types']();
489-
}
490479
#endif // EMBIND
480+
#if MODULARIZE
481+
// The promise resolve function typically gets called as part of the execution
482+
// of the Module `run`. The workers/pthreads don't execute `run` here, they
483+
// call `run` in response to a message at a later time, so the creation
484+
// promise can be resolved, marking the pthread-Module as initialized.
485+
readyPromiseResolve(Module);
486+
#endif // MODULARIZE
487+
}
491488
#else
492489
run();
493-
#endif
490+
#endif // USE_PTHREADS
494491

495492
#if BUILD_AS_WORKER
496493

src/settings.js

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,29 +1029,40 @@ var DETERMINISTIC = 0;
10291029

10301030
// By default we emit all code in a straightforward way into the output
10311031
// .js file. That means that if you load that in a script tag in a web
1032-
// page, it will use the global scope. With MODULARIZE set, we will instead emit
1032+
// page, it will use the global scope. With `MODULARIZE` set, we instead emit
1033+
// the code wrapped in a function that returns a promise. The promise is
1034+
// resolved with the module instance when it is safe to run the compiled code,
1035+
// similar to the `onRuntimeInitialized` callback. You do not need to use the
1036+
// `onRuntimeInitialized` callback when using `MODULARIZE`.
1037+
//
1038+
// The default name of the function is `Module`, but can be changed using the
1039+
// `EXPORT_NAME` option. We recommend renaming it to a more typical name for a
1040+
// factory function, e.g. `createModule`.
10331041
//
1034-
// var EXPORT_NAME = function(Module) {
1035-
// Module = Module || {};
1036-
// // .. all the emitted code from emscripten ..
1037-
// return Module;
1038-
// };
10391042
//
1040-
// where EXPORT_NAME is from the option of the same name (so, by default
1041-
// it will be var Module = ..., and so you should change EXPORT_NAME if
1042-
// you want more than one module in the same web page).
1043+
// You use the factory function like so:
10431044
//
1044-
// You can then use this by something like
1045+
// const module = await EXPORT_NAME();
1046+
//
1047+
// or:
10451048
//
1046-
// var instance = EXPORT_NAME();
1049+
// let module;
1050+
// EXPORT_NAME().then(instance => {
1051+
// module = instance;
1052+
// });
1053+
//
10471054
//
1048-
// or
1055+
// The factory function accepts 1 parameter, an object with default values for
1056+
// the module instance:
10491057
//
1050-
// var instance = EXPORT_NAME({ option: value, ... });
1058+
// const module = await EXPORT_NAME({ option: value, ... });
10511059
//
10521060
// Note the parentheses - we are calling EXPORT_NAME in order to instantiate
1053-
// the module. (This allows, in particular, for you to create multiple
1054-
// instantiations, etc.)
1061+
// the module. This allows you to create multiple instances of the module.
1062+
//
1063+
// Note that in MODULARIZE mode we do *not* look for a global `Module` object
1064+
// for default values. Default values must be passed as a parameter to the
1065+
// factory function.
10551066
//
10561067
// The default .html shell file provided in MINIMAL_RUNTIME mode shows
10571068
// an example to how the module is instantiated from within the html file.
@@ -1062,28 +1073,14 @@ var DETERMINISTIC = 0;
10621073
// https://github.com/emscripten-core/emscripten/issues/7950)
10631074
//
10641075
// If you add --pre-js or --post-js files, they will be included inside
1065-
// the module with the rest of the emitted code. That way, they can be
1066-
// optimized together with it. (If you want something outside of the module,
1067-
// that is, literally before or after all the code including the extra
1068-
// MODULARIZE code, you can do that by modifying the JS yourself after
1069-
// emscripten runs. While --pre-js and --post-js happen to do that in
1070-
// non-modularize mode, their big feature is that they add code to be
1071-
// optimized with the rest of the emitted code, allowing better dead code
1072-
// elimination and minification.)
1073-
//
1074-
// Modularize also provides a promise-like API,
1075-
//
1076-
// var instance = EXPORT_NAME().then(function(Module) { .. });
1077-
//
1078-
// The callback is called when it is safe to run compiled code, similar
1079-
// to the onRuntimeInitialized callback (i.e., it waits for all
1080-
// necessary async events). It receives the instance as a parameter,
1081-
// for convenience.
1076+
// the factory function with the rest of the emitted code in order to be
1077+
// optimized together with it.
10821078
//
1083-
// Note that in MODULARIZE mode we do *not* look at the global `Module`
1084-
// object, so if you define things there they will be ignored. The reason
1085-
// is that you will be constructing the instances manually, and can
1086-
// provide Module there, or something else, as you want.
1079+
// If you want to include code outside all of the generated code, including the
1080+
// factory function, you can use --extern-pre-js or --extern-post-js. While
1081+
// --pre-js and --post-js happen to do that in non-MODULARIZE mode, their
1082+
// intended usage is to add code that is optimized with the rest of the emitted
1083+
// code, allowing better dead code elimination and minification.
10871084
var MODULARIZE = 0;
10881085

10891086
// If we separate out asm.js with the --separate-asm option,

src/shell.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ var Module = typeof {{{ EXPORT_NAME }}} !== 'undefined' ? {{{ EXPORT_NAME }}} :
3838
#endif // USE_CLOSURE_COMPILER
3939
#endif // SIDE_MODULE
4040

41+
#if MODULARIZE
42+
// Set up the promise that indicates the Module is initialized
43+
var readyPromiseResolve, readyPromiseReject;
44+
Module['ready'] = new Promise(function(resolve, reject) {
45+
readyPromiseResolve = resolve;
46+
readyPromiseReject = reject;
47+
});
48+
#endif
49+
4150
// --pre-jses are emitted after the Module integration code, so that they can
4251
// refer to Module (if they choose; they can also define Module)
4352
// {{PRE_JSES}}

src/shell_minimal.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ var Module = {{{ EXPORT_NAME }}};
1515
#endif // USE_CLOSURE_COMPILER
1616
#endif // SIDE_MODULE
1717

18+
#if MODULARIZE
19+
// Set up the promise that indicates the Module is initialized
20+
var readyPromiseResolve, readyPromiseReject;
21+
Module['ready'] = new Promise(function(resolve, reject) {
22+
readyPromiseResolve = resolve;
23+
readyPromiseReject = reject;
24+
});
25+
#endif
26+
1827
#if ENVIRONMENT_MAY_BE_NODE
1928
var ENVIRONMENT_IS_NODE = typeof process === 'object';
2029
#endif
@@ -131,6 +140,9 @@ function err(text) {
131140
// compilation is ready. In that callback, call the function run() to start
132141
// the program.
133142
function ready() {
143+
#if MODULARIZE
144+
readyPromiseResolve(Module);
145+
#endif // MODULARIZE
134146
#if INVOKE_RUN && hasExportedFunction('_main')
135147
#if USE_PTHREADS
136148
if (!ENVIRONMENT_IS_PTHREAD) {

0 commit comments

Comments
 (0)