Asynchronous Code

Emscripten supports two ways (Asyncify and JSPI) that letsynchronous C orC++ code interact withasynchronous JavaScript. This allows things like:

  • A synchronous call in C that yields to the event loop, whichallows browser events to be handled.

  • A synchronous call in C that waits for an asynchronous operation in JS tocomplete.

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

  • Asyncify - Asyncify automatically transforms your compiled code into aform that can be paused and resumed, and handles pausing and resuming foryou, so that it is asynchronous (hence the name “Asyncify”) even though youwrote it in a normal synchronous way. This works in most environments, butcan cause the Wasm output to be much larger.

  • JSPI (experimental) - Uses the VM’s support for JavaScript PromiseIntegration (JSPI) for interacting with async JavaScript. The code size willremain the same, but support for this feature is still experimental.

For more on Asyncify see theAsyncify introduction blogpostfor general background and details of how it works internally (you can also viewthis talk about Asyncify).The following expands on the Emscripten examples from that post.

Sleeping / yielding to the event loop

Let’s begin with the example from that blogpost:

// example.cpp#include<emscripten.h>#include<stdio.h>// start_timer(): call JS to set an async timer for 500msEM_JS(void,start_timer,(),{Module.timer=false;setTimeout(function(){Module.timer=true;},500);});// check_timer(): check if that timer occurredEM_JS(bool,check_timer,(),{returnModule.timer;});intmain(){start_timer();// Continuously loop while synchronously polling for the timer.while(1){if(check_timer()){printf("timer happened!\n");return0;}printf("sleeping...\n");emscripten_sleep(100);}}

You can compile that using either-sASYNCIFY or-sJSPI

emcc-O3example.cpp-s<ASYNCIFYorJSPI>

Note

It’s very important to optimize (-O3 here) when using Asyncify, asunoptimized builds are very large.

And you can run it with

nodejsa.out.js

Or with JSPI

nodejs--experimental-wasm-stack-switchinga.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 whileit is running, which normally would not allow async events to be handled by thebrowser. With Asyncify/JSPI, those sleeps actually yield to the browser’s main eventloop, and the timer can happen!

Making async Web APIs behave as if they were synchronous

Aside fromemscripten_sleep and the other standard sync APIs Asyncifysupports, you can also add your own functions. To do so, you must create a JSfunction that is called from Wasm (since Emscripten controls pausing andresuming the Wasm from the JS runtime).

One way to do that is with a JS library function. Another is to useEM_ASYNC_JS, which we’ll use in this next example:

// example.c#include<emscripten.h>#include<stdio.h>EM_ASYNC_JS(int,do_fetch,(),{out("waiting for a fetch");constresponse=awaitfetch("a.html");out("got the fetch response");// (normally you would do something with the fetch here)return42;});intmain(){puts("before");do_fetch();puts("after");}

In this example the async operation is afetch, which means we need to waitfor a Promise. While that operation is async, note how the C code inmain()is completely synchronous!

To run this example, first compile it with

emccexample.c-O3-oa.html-s<ASYNCIFYorJSPI>

To run this, you must run alocal webserverand then browse tohttp://localhost:8000/a.html.You will see something like this:

beforewaitingforafetchgotthefetchresponseafter

That shows that the C code only continued to execute after the async JScompleted.

Ways to use Asyncify APIs in older engines

If your target JS engine doesn’t support the modernasync/await JSsyntax, you can desugar the above implementation ofdo_fetch to use Promisesdirectly withEM_JS andAsyncify.handleAsync instead:

EM_JS(int,do_fetch,(),{returnAsyncify.handleAsync(function(){out("waiting for a fetch");returnfetch("a.html").then(function(response){out("got the fetch response");// (normally you would do something with the fetch here)return42;});});});

When using this form, the compiler doesn’t statically know thatdo_fetch isasynchronous anymore. Instead, you must tell the compiler thatdo_fetch()can do an asynchronous operation usingASYNCIFY_IMPORTS, otherwise it won’tinstrument the code to allow pausing and resuming (see more details later down):

emccexample.c-O3-oa.html-sASYNCIFY-sASYNCIFY_IMPORTS=do_fetch

Finally, if you can’t use Promises either, you can desugar the example to useAsyncify.handleSleep, which will pass awakeUp callback to yourfunction implementation. When thiswakeUp callback is invoked, the C/C++code will resume:

EM_JS(int,do_fetch,(),{returnAsyncify.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 thewakeUp callback andpropagate it by returning the result ofAsyncify.handleSleep indo_fetchitself.

More onASYNCIFY_IMPORTS

As in the above example, you can add JS functions that do an async operation butlook synchronous from the perspective of C. If you don’t useEM_ASYNC_JS,it’s vital to add such methods toASYNCIFY_IMPORTS. That list of imports isthe list of imports to the Wasm module that the Asyncify instrumentation must beaware of. Giving it that list tells it that all other JS calls willnot doan async operation, which lets it not add overhead where it isn’t needed.

Note

If the import is not insideenv 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 importedfrom other linked modules (and that will be on the stack in an async operation)should be listed inASYNCIFY_IMPORTS.

// sleep.cpp#include<emscripten.h>extern"C"voidsleep_for_seconds(){emscripten_sleep(100);}

In the side module, you can compile sleep.cpp in the ordinal emscripten dynamiclinking manner:

emccsleep.cpp-O3-olibsleep.wasm-sASYNCIFY-sSIDE_MODULE
// main.cpp#include<emscripten.h>extern"C"voidsleep_for_seconds();intmain(){sleep_for_seconds();return0;}

In the main module, the compiler doesn’t statically know thatsleep_for_seconds isasynchronous. Therefore, you must addsleep_for_seconds to theASYNCIFY_IMPORTSlist.

emccmain.cpplibsleep.wasm-O3-sASYNCIFY-sASYNCIFY_IMPORTS=sleep_for_seconds-sMAIN_MODULE

Usage with Embind

If you’re usingEmbind for interaction with JavaScriptand want toawait a dynamically retrievedPromise, you can call anawait() method directly on theval instance:

valmy_object=/* ... */;valresult=my_object.call<val>("someAsyncMethod").await();

In this case you don’t need to worry aboutASYNCIFY_IMPORTS orJSPI_IMPORTS, since it’s an internal implementation detail ofval::awaitand Emscripten takes care of it automatically.

Note that when using Embind exports, Asyncify and JSPI behave differently. WhenAsyncify is used with Embind and the code is invoked from JavaScript, then thefunction will return aPromise if the export calls any suspending functions,otherwise the result will be returned synchronously. However, with JSPI, theparameteremscripten::async() must be used to mark the function asasynchronous and the export will always return aPromise regardless if theexport suspended.

#include<emscripten/bind.h>#include<emscripten.h>staticintdelayAndReturn(boolsleep){if(sleep){emscripten_sleep(0);}return42;}EMSCRIPTEN_BINDINGS(example){// Asyncifyemscripten::function("delayAndReturn",&delayAndReturn);// JSPIemscripten::function("delayAndReturn",&delayAndReturn,emscripten::async());}

Build with

emcc-O3example.cpp-lembind-s<ASYNCIFYorJSPI>

Then invoke from JavaScript (using Asyncify)

letsyncResult=Module.delayAndReturn(false);console.log(syncResult);// 42console.log(awaitsyncResult);// also 42 because `await` is no-opletasyncResult=Module.delayAndReturn(true);console.log(asyncResult);// Promise { <pending> }console.log(awaitasyncResult);// 42

In contrast to JavaScriptasync functions which always return aPromise,the return value is determined at run time, and aPromise is only returnedif Asyncify calls are encountered (such asemscripten_sleep(),val::await(), etc).

If the code path is undetermined, the caller may either check if the returnedvalue is aninstanceofPromise or simplyawait on the returned value.

When using JSPI the return values will always be aPromise as seen below

letsyncResult=Module.delayAndReturn(false);console.log(syncResult);// Promise { <pending> }console.log(awaitsyncResult);// 42letasyncResult=Module.delayAndReturn(true);console.log(asyncResult);// Promise { <pending> }console.log(awaitasyncResult);// 42

Usage withccall

To make use of an Asyncify-using Wasm export from Javascript, you can use theModule.ccall function and passasync:true to its call options object.ccall will then return a Promise, which will resolve with the result of thefunction once the computation completes.

In this example, a function “func” is called which returns a Number.

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 handleasync imports and exports differently. Asyncify will automatically determinewhat exports will become async based on what could potentially call anan async import (ASYNCIFY_IMPORTS). However, with JSPI, the async importsand exports must be explicitly set usingJSPI_IMPORTS andJSPI_EXPORTSsettings.

Note

<JSPI/ASYNCIFY>_IMPORTS andJSPI_EXPORTS aren’t needed whenusing 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 instrumentscode to allow unwinding and rewinding. That overhead is usually not extreme,something like 50% or so. Asyncify achieves that by doing a whole-programanalysis to find functions need to be instrumented and which do not -basically, which can call something that reaches one ofASYNCIFY_IMPORTS. That analysis avoids a lot of unnecessary overhead,however, it is limited byindirect calls, since it can’t tell wherethey 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, thenyou can tell Asyncify to ignore indirect calls usingASYNCIFY_IGNORE_INDIRECT.

If you know that some indirect calls matter and others do not, then youcan 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 callersneed to be instrumented for other reasons.)

  • ASYNCIFY_ADD is a list of functions that do unwind the stack, and will beprocessed like the imports. This is mostly usefulif you useASYNCIFY_IGNORE_INDIRECT but want to also mark some additionalfunctions that need to unwind. If theASYNCIFY_PROPAGATE_ADD setting isdisabled however, then this list will only be added after the whole-programanalysis. IfASYNCIFY_PROPAGATE_ADD is disabled then you must also addtheir callers, their callers’ callers, and so on.

  • ASYNCIFY_ONLY is a list of theonly functions that can unwindthe stack. Asyncify will instrument exactly those and no others.

You can enable theASYNCIFY_ADVISE setting, which will tell the compiler tooutput which functions it is currently instrumenting and why. You can thendetermine whether you should add any functions toASYNCIFY_REMOVE orwhether it would be safe to enableASYNCIFY_IGNORE_INDIRECT. Note that thisphase of the compiler happens after many optimization phases, and severalfunctions maybe be inlined already. To be safe, run it with-O0.

For more details seesettings.js. Note that the manual settingsmentioned 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 anasyncify_* API, then it may bea stack overflow. You can increase the stack size with theASYNCIFY_STACK_SIZE option.

Reentrancy

While waiting on an asynchronous operation browser events can happen. Thatis 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 callemscripten_sleep(100), but if you have any event listeners, say for akeypress, then if a key is pressed the handler will fire. If that handlercalls into compiled code, then it can be confusing, since it starts to looklike coroutines or multithreading, with multiple executions interleaved.

It isnot 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 itreturns, but if that function sleeps and an event causes other code tochange that global, then bad things can happen.

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

The examples above showwakeUp() being called from JS (after a callback,typically), and without any compiled code on the stack. If therewere compiledcode on the stack, then that could interfere with properly rewinding andresuming execution, in confusing ways, and therefore an assertion will bethrown in a build withASSERTIONS.

(Specifically, the problem there is that while rewinding will work properly,if you later unwind again, that unwinding will also unwind through that extracompiled 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, replacingwakeUp() withsetTimeout(wakeUp,0);. That will runwakeUp in alater callback, when nothing else is on the stack.

Migrating from older Asyncify APIs

If you have code uses the old Emterpreter-Async API, or the old Asyncify, thenalmost everything should just work when you replace-sEMTERPRETIFY usagewith-sASYNCIFY. In particular all the things likeemscripten_wgetshould just work as they did before.

Some minor differences include:

  • The Emterpreter had “yielding” as a concept, but it isn’t needed in Asyncify.You can replaceemscripten_sleep_with_yield() calls withemscripten_sleep().

  • The internal JS API is different. See notes above onAsyncify.handleSleep(), and seesrc/library_async.js for moreexamples.