Skip to content

Commit 2bc5e31

Browse files
authored
JSPI - JSPI_<EXPORTS/IMPORTS> setting and docs update. (#21932)
- Replace `ASYNCIFY_IMPORTS` and `ASYNCIFY_EXPORTS` with `JSPI_IMPORTS` and `JSPI_EXPORTS`. - Better document JSPI support.
1 parent 8cc1654 commit 2bc5e31

File tree

7 files changed

+151
-46
lines changed

7 files changed

+151
-46
lines changed

emcc.py

+4
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ def apply_user_settings():
300300

301301
if key == 'JSPI':
302302
settings.ASYNCIFY = 2
303+
if key == 'JSPI_IMPORTS':
304+
settings.ASYNCIFY_IMPORTS = value
305+
if key == 'JSPI_EXPORTS':
306+
settings.ASYNCIFY_EXPORTS = value
303307

304308

305309
def cxx_to_c_compiler(cxx):

site/source/docs/porting/asyncify.rst

+83-32
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
.. _asyncify section:
22

3-
========
4-
Asyncify
5-
========
3+
=================
4+
Asynchronous Code
5+
=================
66

7-
Asyncify lets **synchronous** C or C++ code interact with **asynchronous**
8-
JavaScript. This allows things like:
7+
Emscripten supports two ways (Asyncify and JSPI) that let **synchronous** C or
8+
C++ code interact with **asynchronous** JavaScript. This allows things like:
99

1010
* A synchronous call in C that yields to the event loop, which
1111
allows browser events to be handled.
1212
* A synchronous call in C that waits for an asynchronous operation in JS to
1313
complete.
1414

15-
Asyncify automatically transforms your compiled code into a form that can be
16-
paused and resumed, and handles pausing and resuming for you, so that it is
17-
asynchronous (hence the name "Asyncify") even though you wrote it in a normal
18-
synchronous way.
1915

20-
See the
16+
In general the two options are very similar, but rely on different underlying
17+
mechanisms to work.
18+
19+
* `Asyncify` - Asyncify automatically transforms your compiled code into a
20+
form that can be paused and resumed, and handles pausing and resuming for
21+
you, so that it is asynchronous (hence the name "Asyncify") even though you
22+
wrote it in a normal synchronous way. This works in most environments, but
23+
can cause the Wasm output to be much larger.
24+
* `JSPI` (experimental) - Uses the VM's support for JavaScript Promise
25+
Integration (JSPI) for interacting with async JavaScript. The code size will
26+
remain the same, but support for this feature is still experimental.
27+
28+
For more on Asyncify see the
2129
`Asyncify introduction blogpost <https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html>`_
2230
for general background and details of how it works internally (you can also view
2331
`this talk about Asyncify <https://www.youtube.com/watch?v=qQOP6jqZqf8>`_).
@@ -62,11 +70,11 @@ Let's begin with the example from that blogpost:
6270
}
6371
}
6472
65-
You can compile that with
73+
You can compile that using either `-sASYNCIFY` or `-sJSPI`
6674

6775
::
6876

69-
emcc -O3 example.cpp -sASYNCIFY
77+
emcc -O3 example.cpp -s<ASYNCIFY or JSPI>
7078

7179
.. note:: It's very important to optimize (``-O3`` here) when using Asyncify, as
7280
unoptimized builds are very large.
@@ -77,6 +85,12 @@ And you can run it with
7785

7886
nodejs a.out.js
7987

88+
Or with JSPI
89+
90+
::
91+
92+
nodejs --experimental-wasm-stack-switching a.out.js
93+
8094
You should then see something like this:
8195

8296
::
@@ -90,7 +104,7 @@ You should then see something like this:
90104

91105
The code is written with a straightforward loop, which does not exit while
92106
it is running, which normally would not allow async events to be handled by the
93-
browser. With Asyncify, those sleeps actually yield to the browser's main event
107+
browser. With Asyncify/JSPI, those sleeps actually yield to the browser's main event
94108
loop, and the timer can happen!
95109

96110
Making async Web APIs behave as if they were synchronous
@@ -132,7 +146,7 @@ To run this example, first compile it with
132146

133147
::
134148

135-
emcc example.c -O3 -o a.html -sASYNCIFY
149+
emcc example.c -O3 -o a.html -s<ASYNCIFY or JSPI>
136150

137151
To run this, you must run a :ref:`local webserver <faq-local-webserver>`
138152
and then browse to ``http://localhost:8000/a.html``.
@@ -148,8 +162,8 @@ You will see something like this:
148162
That shows that the C code only continued to execute after the async JS
149163
completed.
150164

151-
Ways to use async APIs in older engines
152-
#######################################
165+
Ways to use Asyncify APIs in older engines
166+
##########################################
153167

154168
If your target JS engine doesn't support the modern ``async/await`` JS
155169
syntax, you can desugar the above implementation of ``do_fetch`` to use Promises
@@ -267,13 +281,17 @@ and want to ``await`` a dynamically retrieved ``Promise``, you can call an
267281
val my_object = /* ... */;
268282
val result = my_object.call<val>("someAsyncMethod").await();
269283
270-
In this case you don't need to worry about ``ASYNCIFY_IMPORTS``, since it's an
271-
internal implementation detail of ``val::await`` and Emscripten takes care of it
272-
automatically.
284+
In this case you don't need to worry about ``ASYNCIFY_IMPORTS`` or
285+
``JSPI_IMPORTS``, since it's an internal implementation detail of ``val::await``
286+
and Emscripten takes care of it automatically.
273287

274-
Note that when Asyncify is used with Embind and the code is invoked from
275-
JavaScript, then it will be implicitly treated as an ``async`` function,
276-
returning a ``Promise`` to the return value, as demonstrated below.
288+
Note that when using Embind exports, Asyncify and JSPI behave differently. When
289+
Asyncify is used with Embind and the code is invoked from JavaScript, then the
290+
function will return a ``Promise`` if the export calls any suspending functions,
291+
otherwise the result will be returned synchronously. However, with JSPI, the
292+
parameter ``emscripten::async()`` must be used to mark the function as
293+
asynchronous and the export will always return a ``Promise`` regardless if the
294+
export suspended.
277295

278296
.. code-block:: cpp
279297
@@ -288,15 +306,18 @@ returning a ``Promise`` to the return value, as demonstrated below.
288306
}
289307
290308
EMSCRIPTEN_BINDINGS(example) {
309+
// Asyncify
291310
emscripten::function("delayAndReturn", &delayAndReturn);
311+
// JSPI
312+
emscripten::function("delayAndReturn", &delayAndReturn, emscripten::async());
292313
}
293314
294315
Build with
295316
::
296317

297-
emcc -O3 example.cpp -lembind -sASYNCIFY
318+
emcc -O3 example.cpp -lembind -s<ASYNCIFY or JSPI>
298319

299-
Then invoke from JavaScript
320+
Then invoke from JavaScript (using Asyncify)
300321

301322
.. code-block:: javascript
302323
@@ -316,6 +337,19 @@ if Asyncify calls are encountered (such as ``emscripten_sleep()``,
316337
If the code path is undetermined, the caller may either check if the returned
317338
value is an ``instanceof Promise`` or simply ``await`` on the returned value.
318339

340+
When using JSPI the return values will always be a ``Promise`` as seen below
341+
342+
.. code-block:: javascript
343+
344+
let syncResult = Module.delayAndReturn(false);
345+
console.log(syncResult); // Promise { <pending> }
346+
console.log(await syncResult); // 42
347+
348+
let asyncResult = Module.delayAndReturn(true);
349+
console.log(asyncResult); // Promise { <pending> }
350+
console.log(await asyncResult); // 42
351+
352+
319353
Usage with ``ccall``
320354
####################
321355

@@ -332,8 +366,25 @@ In this example, a function "func" is called which returns a Number.
332366
console.log("js_func: " + result);
333367
});
334368
335-
Optimizing
336-
##########
369+
370+
Differences Between Asyncify and JSPI
371+
#####################################
372+
373+
Besides using different underlying mechanisms, Asyncify and JSPI also handle
374+
async imports and exports differently. Asyncify will automatically determine
375+
what exports will become async based on what could potentially call an
376+
an async import (``ASYNCIFY_IMPORTS``). However, with JSPI, the async imports
377+
and exports must be explicitly set using ``JSPI_IMPORTS`` and ``JSPI_EXPORTS``
378+
settings.
379+
380+
.. note:: ``<JSPI/ASYNCIFY>_IMPORTS`` and ``JSPI_EXPORTS`` aren't needed when
381+
using various helpers mentioned above such as: ``EM_ASYNC_JS``,
382+
Embind's Async support, ``ccall``, etc...
383+
384+
Optimizing Asyncify
385+
###################
386+
387+
.. note:: This section does not apply to JSPI.
337388

338389
As mentioned earlier, unoptimized builds with Asyncify can be large and slow.
339390
Build with optimizations (say, ``-O3``) to get good results.
@@ -383,8 +434,8 @@ it's usually ok to use the defaults.
383434
Potential problems
384435
##################
385436

386-
Stack overflows
387-
***************
437+
Stack overflows (Asyncify)
438+
**************************
388439

389440
If you see an exception thrown from an ``asyncify_*`` API, then it may be
390441
a stack overflow. You can increase the stack size with the
@@ -409,8 +460,8 @@ if a function uses a global and assumes nothing else can modify it until it
409460
returns, but if that function sleeps and an event causes other code to
410461
change that global, then bad things can happen.
411462

412-
Starting to rewind with compiled code on the stack
413-
**************************************************
463+
Starting to rewind with compiled code on the stack (Asyncify)
464+
*************************************************************
414465

415466
The examples above show `wakeUp()` being called from JS (after a callback,
416467
typically), and without any compiled code on the stack. If there *were* compiled
@@ -426,8 +477,8 @@ A simple workaround you may find useful is to do a setTimeout of 0, replacing
426477
``wakeUp()`` with ``setTimeout(wakeUp, 0);``. That will run ``wakeUp`` in a
427478
later callback, when nothing else is on the stack.
428479

429-
Migrating from older APIs
430-
#########################
480+
Migrating from older Asyncify APIs
481+
##################################
431482

432483
If you have code uses the old Emterpreter-Async API, or the old Asyncify, then
433484
almost everything should just work when you replace ``-sEMTERPRETIFY`` usage

site/source/docs/tools_reference/settings_reference.rst

+32-4
Original file line numberDiff line numberDiff line change
@@ -1223,7 +1223,7 @@ Default value: 0
12231223
ASYNCIFY_IMPORTS
12241224
================
12251225

1226-
Imports which can do an sync operation, in addition to the default ones that
1226+
Imports which can do an async operation, in addition to the default ones that
12271227
emscripten defines like emscripten_sleep. If you add more you will need to
12281228
mention them to here, or else they will not work (in ASSERTIONS builds an
12291229
error will be shown).
@@ -1379,9 +1379,9 @@ Default value: 0
13791379
ASYNCIFY_EXPORTS
13801380
================
13811381

1382-
Specify which of the exports will have JSPI applied to them and return a
1383-
promise.
1384-
Only supported for ASYNCIFY==2 mode.
1382+
Deprecated, use JSPI_EXPORTS instead.
1383+
1384+
.. note:: This setting is deprecated
13851385

13861386
Default value: []
13871387

@@ -1399,6 +1399,34 @@ etc. are not needed)
13991399

14001400
Default value: 0
14011401

1402+
.. _jspi_exports:
1403+
1404+
JSPI_EXPORTS
1405+
============
1406+
1407+
A list of exported module functions that will be asynchronous. Each export
1408+
will return a ``Promise`` that will be resolved with the result. Any exports
1409+
that will call an asynchronous import (listed in ``JSPI_IMPORTS``) must be
1410+
included here.
1411+
1412+
By default this includes ``main``.
1413+
1414+
Default value: []
1415+
1416+
.. _jspi_imports:
1417+
1418+
JSPI_IMPORTS
1419+
============
1420+
1421+
A list of imported module functions that will potentially do asynchronous
1422+
work. The imported function should return a ``Promise`` when doing
1423+
asynchronous work.
1424+
1425+
Note when using ``--js-library``, the function can be marked with
1426+
``<function_name>_async:: true`` in the library instead of this setting.
1427+
1428+
Default value: []
1429+
14021430
.. _exported_runtime_methods:
14031431

14041432
EXPORTED_RUNTIME_METHODS

src/settings.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ var NODEJS_CATCH_REJECTION = true;
819819
// [link]
820820
var ASYNCIFY = 0;
821821

822-
// Imports which can do an sync operation, in addition to the default ones that
822+
// Imports which can do an async operation, in addition to the default ones that
823823
// emscripten defines like emscripten_sleep. If you add more you will need to
824824
// mention them to here, or else they will not work (in ASSERTIONS builds an
825825
// error will be shown).
@@ -927,10 +927,8 @@ var ASYNCIFY_LAZY_LOAD_CODE = false;
927927
// [link]
928928
var ASYNCIFY_DEBUG = 0;
929929

930-
// Specify which of the exports will have JSPI applied to them and return a
931-
// promise.
932-
// Only supported for ASYNCIFY==2 mode.
933-
// [link]
930+
// Deprecated, use JSPI_EXPORTS instead.
931+
// [deprecated]
934932
var ASYNCIFY_EXPORTS = [];
935933

936934
// Use VM support for the JavaScript Promise Integration proposal. This allows
@@ -943,6 +941,25 @@ var ASYNCIFY_EXPORTS = [];
943941
// [link]
944942
var JSPI = 0;
945943

944+
// A list of exported module functions that will be asynchronous. Each export
945+
// will return a ``Promise`` that will be resolved with the result. Any exports
946+
// that will call an asynchronous import (listed in ``JSPI_IMPORTS``) must be
947+
// included here.
948+
//
949+
// By default this includes ``main``.
950+
// [link]
951+
var JSPI_EXPORTS = [];
952+
953+
954+
// A list of imported module functions that will potentially do asynchronous
955+
// work. The imported function should return a ``Promise`` when doing
956+
// asynchronous work.
957+
//
958+
// Note when using ``--js-library``, the function can be marked with
959+
// ``<function_name>_async:: true`` in the library instead of this setting.
960+
// [link]
961+
var JSPI_IMPORTS = [];
962+
946963
// Runtime elements that are exported on Module by default. We used to export
947964
// quite a lot here, but have removed them all. You should use
948965
// EXPORTED_RUNTIME_METHODS for things you want to export from the runtime.

test/test_core.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8171,7 +8171,7 @@ def test_async_ccall_good(self):
81718171
def test_async_ccall_promise(self, exit_runtime, asyncify):
81728172
if asyncify == 2:
81738173
self.require_jspi()
8174-
self.set_setting('ASYNCIFY_EXPORTS', ['stringf', 'floatf'])
8174+
self.set_setting('JSPI_EXPORTS', ['stringf', 'floatf'])
81758175
self.set_setting('ASYNCIFY', asyncify)
81768176
self.set_setting('EXIT_RUNTIME')
81778177
self.set_setting('ASSERTIONS')
@@ -8348,7 +8348,7 @@ def test_pthread_join_and_asyncify(self):
83488348
# TODO Test with ASYNCIFY=1 https://github.com/emscripten-core/emscripten/issues/17552
83498349
self.require_jspi()
83508350
self.do_runf('core/test_pthread_join_and_asyncify.c', 'joining thread!\njoined thread!',
8351-
emcc_args=['-sASYNCIFY_EXPORTS=run_thread',
8351+
emcc_args=['-sJSPI_EXPORTS=run_thread',
83528352
'-sEXIT_RUNTIME=1',
83538353
'-pthread', '-sPROXY_TO_PTHREAD'])
83548354

test/test_other.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -3202,9 +3202,13 @@ def test_embind_return_value_policy(self):
32023202

32033203
self.do_runf('embind/test_return_value_policy.cpp')
32043204

3205-
def test_jspi_wildcard(self):
3205+
@parameterized({
3206+
'': [['-sJSPI_EXPORTS=async*']],
3207+
'deprecated': [['-Wno-deprecated', '-sASYNCIFY_EXPORTS=async*']]
3208+
})
3209+
def test_jspi_wildcard(self, opts):
32063210
self.require_jspi()
3207-
self.emcc_args += ['-sASYNCIFY_EXPORTS=async*']
3211+
self.emcc_args += opts
32083212

32093213
self.do_runf('other/test_jspi_wildcard.c', 'done')
32103214

@@ -12579,7 +12583,7 @@ def test_split_module(self, customLoader, jspi):
1257912583
self.emcc_args += ['--pre-js', test_file('other/test_load_split_module.pre.js')]
1258012584
if jspi:
1258112585
self.require_jspi()
12582-
self.emcc_args += ['-g', '-sASYNCIFY_EXPORTS=say_hello']
12586+
self.emcc_args += ['-g', '-sJSPI_EXPORTS=say_hello']
1258312587
self.emcc_args += ['-sEXPORTED_FUNCTIONS=_malloc,_free']
1258412588
output = self.do_other_test('test_split_module.c')
1258512589
if jspi:

tools/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
'RUNTIME_LINKED_LIBS': 'you can simply list the libraries directly on the commandline now',
124124
'CLOSURE_WARNINGS': 'use -Wclosure instead',
125125
'LEGALIZE_JS_FFI': 'to disable JS type legalization use `-sWASM_BIGINT` or `-sSTANDALONE_WASM`',
126+
'ASYNCIFY_EXPORTS': 'please use JSPI_EXPORTS instead'
126127
}
127128

128129
# Settings that don't need to be externalized when serializing to json because they

0 commit comments

Comments
 (0)