site/source/docs/porting/asyncify.rst
.. _asyncify section:
Emscripten supports two ways (Asyncify and JSPI) that let synchronous C or C++ code interact with asynchronous JavaScript. This allows things like:
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>_).
The following expands on the Emscripten examples from that post.
.. _yielding_to_main_loop:
Sleeping / yielding to the event loop #####################################
Let's begin with the example from that blogpost:
.. code-block:: cpp
// example.cpp
#include <emscripten.h>
#include <stdio.h>
// start_timer(): call JS to set an async timer for 500ms
EM_JS(void, start_timer, (), {
Module.timer = false;
setTimeout(function() {
Module.timer = true;
}, 500);
});
// check_timer(): check if that timer occurred
EM_JS(bool, check_timer, (), {
return Module.timer;
});
int main() {
start_timer();
// Continuously loop while synchronously polling for the timer.
while (1) {
if (check_timer()) {
printf("timer happened!\n");
return 0;
}
printf("sleeping...\n");
emscripten_sleep(100);
}
}
You can compile that using either -sASYNCIFY or -sJSPI
::
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.
And you can run it with
::
node a.out.js
Or with JSPI
::
node --experimental-wasm-stack-switching a.out.js
You should then see something like this:
::
sleeping...
sleeping...
sleeping...
sleeping...
sleeping...
timer happened!
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/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 ########################################################
Aside from emscripten_sleep and the other standard sync APIs Asyncify
supports, you can also add your own functions. To do so, you must create a JS
function that is called from Wasm (since Emscripten controls pausing and
resuming the Wasm from the JS runtime).
One way to do that is with a JS library function. Another is to use
EM_ASYNC_JS, which we'll use in this next example:
.. code-block:: cpp
// example.c
#include <emscripten.h>
#include <stdio.h>
EM_ASYNC_JS(int, do_fetch, (), {
out("waiting for a fetch");
const response = await fetch("a.html");
out("got the fetch response");
// (normally you would do something with the fetch here)
return 42;
});
int main() {
puts("before");
do_fetch();
puts("after");
}
In this example the async operation is a fetch, which means we need to wait
for a Promise. While that operation is async, note how the C code in main()
is completely synchronous!
To run this example, first compile it with
::
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.
You will see something like this:
::
before
waiting for a fetch
got the fetch response
after
That shows that the C code only continued to execute after the async JS completed.
.. _marking_async_functions:
Marking JS library functions as async #####################################
If you mark a JS library function as async using the __async decorator then
the compiler will take a care of all the details of using the Asyncify API
for you. The function will also automatically be included in
:ref:ASYNCIFY_IMPORTS. All you need to do is write normal async JS function
(either using the explict async JS keyword or returning a Promise
object). For example:
.. code-block:: js
addToLibrary({ fetch_v1__async: 'auto', fetch_v1: async (url) => { const response = await fetch(UTF8ToString(url); const json_data = await response.json(); return stringToNewUTF8(json_data); },
fetch_v2__async: 'auto',
fetch_v2: (url) => {
return fetch(UTF8ToString(url))
.then((rsp) => response.json())
.then((json_data) => stringToNewUTF8(json_data));
},
});
This JS library file contains two JS functions (fetch_v1 and fetch_v2)
which do exactly the same thing, one using the JS async keyword and one
using promise chaining. They are both marked as __async: 'auto' which means
they will automatically suspend Wasm executation when called and resume when the
resulting promise resolves.
You can use __async: 1 if you just want to include the function in
:ref:ASYNCIFY_IMPORTS or __async: 'auto' if you also want the function to
wrapper in Asyncify.handleAsync.
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
directly with EM_JS and Asyncify.handleAsync instead:
.. code-block:: cpp
EM_JS(int, do_fetch, (), {
return Asyncify.handleAsync(function () {
out("waiting for a fetch");
return fetch("a.html").then(function (response) {
out("got the fetch response");
// (normally you would do something with the fetch here)
return 42;
});
});
});
When using this form, the compiler doesn't statically know that do_fetch is
asynchronous anymore. Instead, you must tell the compiler that do_fetch()
can do an asynchronous operation using :ref:ASYNCIFY_IMPORTS, otherwise it
won't instrument the code to allow pausing and resuming (see more details later
down):
::
emcc example.c -O3 -o a.html -sASYNCIFY -sASYNCIFY_IMPORTS=do_fetch
Finally, if you can't use Promises either, you can desugar the example to use
Asyncify.handleSleep, which will pass a wakeUp callback to your
function implementation. When this wakeUp callback is invoked, the C/C++
code will resume:
.. code-block:: cpp
EM_JS(int, do_fetch, (), {
return Asyncify.handleSleep((wakeUp) => {
out("waiting for a fetch");
fetch("a.html").then(function (response) {
out("got the fetch response");
// (normally you would do something with the fetch here)
wakeUp(42);
});
});
});
Note that when using this form, you can't return a value from the function itself.
Instead, you need to pass it as an argument to the wakeUp callback and
propagate it by returning the result of Asyncify.handleSleep in do_fetch
itself.
More on ASYNCIFY_IMPORTS
############################
As in the above example, you can add JS functions that do an async operation but
look synchronous from the perspective of C. If you don't use EM_ASYNC_JS,
it's vital to add such methods to :ref:ASYNCIFY_IMPORTS. That list of imports
is the list of imports to the Wasm module that the Asyncify instrumentation must
be aware of. Giving it that list tells it that all other JS calls will not
do an async operation, which lets it not add overhead where it isn't needed.
.. note:: If the import is not inside env the full path must be specified, for example, ASYNCIFY_IMPORTS=wasi_snapshot_preview1.fd_write
Asyncify with Dynamic Linking #############################
If you want to use Asyncify in dynamic libraries, those methods which are imported
from other linked modules (and that will be on the stack in an async operation)
should be listed in :ref:ASYNCIFY_IMPORTS.
.. code-block:: cpp
// sleep.cpp
#include <emscripten.h>
extern "C" void sleep_for_seconds() {
emscripten_sleep(100);
}
In the side module, you can compile sleep.cpp in the ordinal emscripten dynamic linking manner:
::
emcc sleep.cpp -O3 -o libsleep.wasm -sASYNCIFY -sSIDE_MODULE
.. code-block:: cpp
// main.cpp
#include <emscripten.h>
extern "C" void sleep_for_seconds();
int main() {
sleep_for_seconds();
return 0;
}
In the main module, the compiler doesn’t statically know that sleep_for_seconds is
asynchronous. Therefore, you must add sleep_for_seconds to the
:ref:ASYNCIFY_IMPORTS list.
::
emcc main.cpp libsleep.wasm -O3 -sASYNCIFY -sASYNCIFY_IMPORTS=sleep_for_seconds -sMAIN_MODULE
Usage with Embind #################
If you're using :ref:Embind<embind-val-guide> for interaction with JavaScript
and want to await a dynamically retrieved Promise, you can call an
await() method directly on the val instance:
.. code-block:: cpp
val my_object = /* ... */;
val result = my_object.call<val>("someAsyncMethod").await();
In this case you don't need to worry about :ref:ASYNCIFY_IMPORTS or
:ref:JSPI_IMPORTS, since it's an internal implementation detail of
val::await and Emscripten takes care of it automatically.
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
#include <emscripten/bind.h> #include <emscripten.h>
static int delayAndReturn(bool sleep) { if (sleep) { emscripten_sleep(0); } return 42; }
EMSCRIPTEN_BINDINGS(example) { // Asyncify emscripten::function("delayAndReturn", &delayAndReturn); // JSPI emscripten::function("delayAndReturn", &delayAndReturn, emscripten::async()); }
Build with ::
emcc -O3 example.cpp -lembind -s<ASYNCIFY or JSPI>
Then invoke from JavaScript (using Asyncify)
.. code-block:: javascript
let syncResult = Module.delayAndReturn(false);
console.log(syncResult); // 42
console.log(await syncResult); // also 42 because await is no-op
let asyncResult = Module.delayAndReturn(true); console.log(asyncResult); // Promise { <pending> } console.log(await asyncResult); // 42
In contrast to JavaScript async functions which always return a Promise,
the return value is determined at run time, and a Promise is only returned
if Asyncify calls are encountered (such as emscripten_sleep(),
val::await(), etc).
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
####################
To make use of an Asyncify-using Wasm export from Javascript, you can use the
Module.ccall function and pass async: true to its call options object.
ccall will then return a Promise, which will resolve with the result of the
function once the computation completes.
In this example, a function "func" is called which returns a Number.
.. code-block:: javascript
Module.ccall("func", "number", [], [], {async: true}).then(result => {
console.log("js_func: " + result);
});
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 (:ref:ASYNCIFY_IMPORTS). However, with JSPI, the async imports
and exports must be explicitly set using :ref:JSPI_IMPORTS and
:ref: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.
Asyncify adds overhead, both code size and slowness, because it instruments
code to allow unwinding and rewinding. That overhead is usually not extreme,
something like 50% or so. Asyncify achieves that by doing a whole-program
analysis to find functions need to be instrumented and which do not -
basically, which can call something that reaches one of
:ref:ASYNCIFY_IMPORTS. That analysis avoids a lot of unnecessary overhead,
however, it is limited by indirect calls, since it can't tell where
they go - it could be anything in the function table (with the same type).
If you know that indirect calls are never on the stack when unwinding, then
you can tell Asyncify to ignore indirect calls using
:ref:ASYNCIFY_IGNORE_INDIRECT.
If you know that some indirect calls matter and others do not, then you can provide a manual list of functions to Asyncify:
ASYNCIFY_REMOVE is a list of functions that do not unwind the stack.
As Asyncify processes the call tree, functions in this list will be removed,
and neither they nor their callers will be instrumented (unless their callers
need to be instrumented for other reasons.)ASYNCIFY_ADD is a list of functions that do unwind the stack, and will be
processed like the imports. This is mostly useful
if you use :ref:ASYNCIFY_IGNORE_INDIRECT but want to also mark some additional
functions that need to unwind. If the :ref:ASYNCIFY_PROPAGATE_ADD setting is
disabled however, then this list will only be added after the whole-program
analysis. If :ref:ASYNCIFY_PROPAGATE_ADD is disabled then you must also add
their callers, their callers' callers, and so on.ASYNCIFY_ONLY is a list of the only functions that can unwind
the stack. Asyncify will instrument exactly those and no others.You can enable the :ref:ASYNCIFY_ADVISE setting, which will tell the compiler to
output which functions it is currently instrumenting and why. You can then
determine whether you should add any functions to :ref:ASYNCIFY_REMOVE or
whether it would be safe to enable :ref:ASYNCIFY_IGNORE_INDIRECT. Note that this
phase of the compiler happens after many optimization phases, and several
functions maybe be inlined already. To be safe, run it with -O0.
For more details see :ref:settings-reference. Note that the manual settings
mentioned here are error-prone - if you don't get things exactly right,
your application can break. If you don't absolutely need maximal performance,
it's usually ok to use the defaults.
Potential problems ##################
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
:ref:ASYNCIFY_STACK_SIZE option.
Reentrancy
While waiting on an asynchronous operation browser events can happen. That
is often the point of using Asyncify, but unexpected events can happen too.
For example, if you just want to pause for 100ms then you can call
emscripten_sleep(100), but if you have any event listeners, say for a
keypress, then if a key is pressed the handler will fire. If that handler
calls into compiled code, then it can be confusing, since it starts to look
like coroutines or multithreading, with multiple executions interleaved.
It is not safe to start an async operation while another is already running. The first must complete before the second begins.
Such interleaving may also break assumptions in your codebase. For example, 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 (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
code on the stack, then that could interfere with properly rewinding and
resuming execution, in confusing ways, and therefore an assertion will be
thrown in a build with :ref:ASSERTIONS.
(Specifically, the problem there is that while rewinding will work properly, if you later unwind again, that unwinding will also unwind through that extra compiled code that was on the stack - causing a later rewind to behave badly.)
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 Asyncify APIs ##################################
If you have code that uses the old Emterpreter-Async API, or the old Asyncify, then
almost everything should just work when you replace -sEMTERPRETIFY usage
with -sASYNCIFY. In particular all the things like emscripten_wget
should just work as they did before.
Some minor differences include:
emscripten_sleep_with_yield() calls with emscripten_sleep().Asyncify.handleSleep(), and see src/library_async.js for more
examples.