Skip to content

Commit

Permalink
JSPI - JSPI_<EXPORTS/IMPORTS> setting and docs update. (#21932)
Browse files Browse the repository at this point in the history
- Replace `ASYNCIFY_IMPORTS` and `ASYNCIFY_EXPORTS` with
`JSPI_IMPORTS` and `JSPI_EXPORTS`.
- Better document JSPI support.
  • Loading branch information
brendandahl committed May 14, 2024
1 parent 8cc1654 commit 2bc5e31
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 46 deletions.
4 changes: 4 additions & 0 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ def apply_user_settings():

if key == 'JSPI':
settings.ASYNCIFY = 2
if key == 'JSPI_IMPORTS':
settings.ASYNCIFY_IMPORTS = value
if key == 'JSPI_EXPORTS':
settings.ASYNCIFY_EXPORTS = value


def cxx_to_c_compiler(cxx):
Expand Down
115 changes: 83 additions & 32 deletions site/source/docs/porting/asyncify.rst
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
.. _asyncify section:

========
Asyncify
========
=================
Asynchronous Code
=================

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

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

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

See the
In general the two options are very similar, but rely on different underlying
mechanisms to work.

* `Asyncify` - Asyncify automatically transforms your compiled code into a
form that can be paused and resumed, and handles pausing and resuming for
you, so that it is asynchronous (hence the name "Asyncify") even though you
wrote it in a normal synchronous way. This works in most environments, but
can cause the Wasm output to be much larger.
* `JSPI` (experimental) - Uses the VM's support for JavaScript Promise
Integration (JSPI) for interacting with async JavaScript. The code size will
remain the same, but support for this feature is still experimental.

For more on Asyncify see the
`Asyncify introduction blogpost <https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html>`_
for general background and details of how it works internally (you can also view
`this talk about Asyncify <https://www.youtube.com/watch?v=qQOP6jqZqf8>`_).
Expand Down Expand Up @@ -62,11 +70,11 @@ Let's begin with the example from that blogpost:
}
}
You can compile that with
You can compile that using either `-sASYNCIFY` or `-sJSPI`

::

emcc -O3 example.cpp -sASYNCIFY
emcc -O3 example.cpp -s<ASYNCIFY or JSPI>

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

nodejs a.out.js

Or with JSPI

::

nodejs --experimental-wasm-stack-switching a.out.js

You should then see something like this:

::
Expand All @@ -90,7 +104,7 @@ You should then see something like this:

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

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

::

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

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

Ways to use async APIs in older engines
#######################################
Ways to use Asyncify APIs in older engines
##########################################

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

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

.. code-block:: cpp
Expand All @@ -288,15 +306,18 @@ returning a ``Promise`` to the return value, as demonstrated below.
}
EMSCRIPTEN_BINDINGS(example) {
// Asyncify
emscripten::function("delayAndReturn", &delayAndReturn);
// JSPI
emscripten::function("delayAndReturn", &delayAndReturn, emscripten::async());
}
Build with
::

emcc -O3 example.cpp -lembind -sASYNCIFY
emcc -O3 example.cpp -lembind -s<ASYNCIFY or JSPI>

Then invoke from JavaScript
Then invoke from JavaScript (using Asyncify)

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

When using JSPI the return values will always be a ``Promise`` as seen below

.. code-block:: javascript
let syncResult = Module.delayAndReturn(false);
console.log(syncResult); // Promise { <pending> }
console.log(await syncResult); // 42
let asyncResult = Module.delayAndReturn(true);
console.log(asyncResult); // Promise { <pending> }
console.log(await asyncResult); // 42
Usage with ``ccall``
####################

Expand All @@ -332,8 +366,25 @@ In this example, a function "func" is called which returns a Number.
console.log("js_func: " + result);
});
Optimizing
##########
Differences Between Asyncify and JSPI
#####################################

Besides using different underlying mechanisms, Asyncify and JSPI also handle
async imports and exports differently. Asyncify will automatically determine
what exports will become async based on what could potentially call an
an async import (``ASYNCIFY_IMPORTS``). However, with JSPI, the async imports
and exports must be explicitly set using ``JSPI_IMPORTS`` and ``JSPI_EXPORTS``
settings.

.. note:: ``<JSPI/ASYNCIFY>_IMPORTS`` and ``JSPI_EXPORTS`` aren't needed when
using various helpers mentioned above such as: ``EM_ASYNC_JS``,
Embind's Async support, ``ccall``, etc...

Optimizing Asyncify
###################

.. note:: This section does not apply to JSPI.

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

Stack overflows
***************
Stack overflows (Asyncify)
**************************

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

Starting to rewind with compiled code on the stack
**************************************************
Starting to rewind with compiled code on the stack (Asyncify)
*************************************************************

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

Migrating from older APIs
#########################
Migrating from older Asyncify APIs
##################################

If you have code uses the old Emterpreter-Async API, or the old Asyncify, then
almost everything should just work when you replace ``-sEMTERPRETIFY`` usage
Expand Down
36 changes: 32 additions & 4 deletions site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,7 @@ Default value: 0
ASYNCIFY_IMPORTS
================

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

Specify which of the exports will have JSPI applied to them and return a
promise.
Only supported for ASYNCIFY==2 mode.
Deprecated, use JSPI_EXPORTS instead.

.. note:: This setting is deprecated

Default value: []

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

Default value: 0

.. _jspi_exports:

JSPI_EXPORTS
============

A list of exported module functions that will be asynchronous. Each export
will return a ``Promise`` that will be resolved with the result. Any exports
that will call an asynchronous import (listed in ``JSPI_IMPORTS``) must be
included here.

By default this includes ``main``.

Default value: []

.. _jspi_imports:

JSPI_IMPORTS
============

A list of imported module functions that will potentially do asynchronous
work. The imported function should return a ``Promise`` when doing
asynchronous work.

Note when using ``--js-library``, the function can be marked with
``<function_name>_async:: true`` in the library instead of this setting.

Default value: []

.. _exported_runtime_methods:

EXPORTED_RUNTIME_METHODS
Expand Down
27 changes: 22 additions & 5 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ var NODEJS_CATCH_REJECTION = true;
// [link]
var ASYNCIFY = 0;

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

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

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

// A list of exported module functions that will be asynchronous. Each export
// will return a ``Promise`` that will be resolved with the result. Any exports
// that will call an asynchronous import (listed in ``JSPI_IMPORTS``) must be
// included here.
//
// By default this includes ``main``.
// [link]
var JSPI_EXPORTS = [];


// A list of imported module functions that will potentially do asynchronous
// work. The imported function should return a ``Promise`` when doing
// asynchronous work.
//
// Note when using ``--js-library``, the function can be marked with
// ``<function_name>_async:: true`` in the library instead of this setting.
// [link]
var JSPI_IMPORTS = [];

// Runtime elements that are exported on Module by default. We used to export
// quite a lot here, but have removed them all. You should use
// EXPORTED_RUNTIME_METHODS for things you want to export from the runtime.
Expand Down
4 changes: 2 additions & 2 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8171,7 +8171,7 @@ def test_async_ccall_good(self):
def test_async_ccall_promise(self, exit_runtime, asyncify):
if asyncify == 2:
self.require_jspi()
self.set_setting('ASYNCIFY_EXPORTS', ['stringf', 'floatf'])
self.set_setting('JSPI_EXPORTS', ['stringf', 'floatf'])
self.set_setting('ASYNCIFY', asyncify)
self.set_setting('EXIT_RUNTIME')
self.set_setting('ASSERTIONS')
Expand Down Expand Up @@ -8348,7 +8348,7 @@ def test_pthread_join_and_asyncify(self):
# TODO Test with ASYNCIFY=1 https://github.com/emscripten-core/emscripten/issues/17552
self.require_jspi()
self.do_runf('core/test_pthread_join_and_asyncify.c', 'joining thread!\njoined thread!',
emcc_args=['-sASYNCIFY_EXPORTS=run_thread',
emcc_args=['-sJSPI_EXPORTS=run_thread',
'-sEXIT_RUNTIME=1',
'-pthread', '-sPROXY_TO_PTHREAD'])

Expand Down
10 changes: 7 additions & 3 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -3202,9 +3202,13 @@ def test_embind_return_value_policy(self):

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

def test_jspi_wildcard(self):
@parameterized({
'': [['-sJSPI_EXPORTS=async*']],
'deprecated': [['-Wno-deprecated', '-sASYNCIFY_EXPORTS=async*']]
})
def test_jspi_wildcard(self, opts):
self.require_jspi()
self.emcc_args += ['-sASYNCIFY_EXPORTS=async*']
self.emcc_args += opts

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

Expand Down Expand Up @@ -12579,7 +12583,7 @@ def test_split_module(self, customLoader, jspi):
self.emcc_args += ['--pre-js', test_file('other/test_load_split_module.pre.js')]
if jspi:
self.require_jspi()
self.emcc_args += ['-g', '-sASYNCIFY_EXPORTS=say_hello']
self.emcc_args += ['-g', '-sJSPI_EXPORTS=say_hello']
self.emcc_args += ['-sEXPORTED_FUNCTIONS=_malloc,_free']
output = self.do_other_test('test_split_module.c')
if jspi:
Expand Down
1 change: 1 addition & 0 deletions tools/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
'RUNTIME_LINKED_LIBS': 'you can simply list the libraries directly on the commandline now',
'CLOSURE_WARNINGS': 'use -Wclosure instead',
'LEGALIZE_JS_FFI': 'to disable JS type legalization use `-sWASM_BIGINT` or `-sSTANDALONE_WASM`',
'ASYNCIFY_EXPORTS': 'please use JSPI_EXPORTS instead'
}

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

0 comments on commit 2bc5e31

Please sign in to comment.