Skip to content

Commit

Permalink
Asyncify: Add support for dynamic linking (#15893)
Browse files Browse the repository at this point in the history
Fixes #15594

Before Asyncify wouldn't work properly in side modules, because the
globals, __asyncify_state and __asyncify_data, are not synchronized
between main-module and side-modules.

With WebAssembly/binaryen#4427, __asyncify_state and __asyncify_data
will be imported globals in the side modules.
  • Loading branch information
nokotan authored Jul 19, 2022
1 parent d7892a9 commit 7f8dfa7
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 0 deletions.
9 changes: 9 additions & 0 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,8 @@ def get_binaryen_passes():
passes += ['--fpcast-emu']
if settings.ASYNCIFY == 1:
passes += ['--asyncify']
if settings.MAIN_MODULE or settings.SIDE_MODULE:
passes += ['--pass-arg=asyncify-relocatable']
if settings.ASSERTIONS:
passes += ['--pass-arg=asyncify-asserts']
if settings.ASYNCIFY_ADVISE:
Expand Down Expand Up @@ -1906,6 +1908,13 @@ def phase_linker_setup(options, state, newargs, user_settings):
'__heap_base',
'__stack_pointer',
]

if settings.ASYNCIFY:
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += [
'__asyncify_state',
'__asyncify_data'
]

# Unconditional dependency in library_dylink.js
settings.REQUIRED_EXPORTS += ['setThrew']

Expand Down
3 changes: 3 additions & 0 deletions emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,9 @@ def emscript(in_wasm, out_wasm, outfile_js, memfile):
if settings.INITIAL_TABLE == -1:
settings.INITIAL_TABLE = dylink_sec.table_size + 1

if settings.ASYNCIFY:
metadata['globalImports'] += ['__asyncify_state', '__asyncify_data']

invoke_funcs = metadata['invokeFuncs']
if invoke_funcs:
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$getWasmTableEntry']
Expand Down
43 changes: 43 additions & 0 deletions site/source/docs/porting/asyncify.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,49 @@ 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.

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 ``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 ``ASYNCIFY_IMPORTS``
list.

::

emcc main.cpp libsleep.wasm -O3 -sASYNCIFY -sASYNCIFY_IMPORTS=sleep_for_seconds -sMAIN_MODULE

Usage with Embind
#################

Expand Down
6 changes: 6 additions & 0 deletions src/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -3628,6 +3628,12 @@ mergeInto(LibraryManager.library, {
__c_longjmp: "new WebAssembly.Tag({'parameters': ['{{{ POINTER_WASM_TYPE }}}']})",
__c_longjmp_import: true,
#endif
#if ASYNCIFY
__asyncify_state: "new WebAssembly.Global({'value': 'i32', 'mutable': true}, 0)",
__asyncify_state__import: true,
__asyncify_data: "new WebAssembly.Global({'value': 'i32', 'mutable': true}, 0)",
__asyncify_data__import: true,
#endif
#endif

_emscripten_fs_load_embedded_files__deps: ['$FS', '$PATH'],
Expand Down
13 changes: 13 additions & 0 deletions src/library_async.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ mergeInto(LibraryManager.library, {
}
}
};
#if MAIN_MODULE
ret[x].orig = original;
#endif
} else {
ret[x] = original;
}
Expand Down Expand Up @@ -260,10 +263,20 @@ mergeInto(LibraryManager.library, {
{{{ makeSetValue('ptr', C_STRUCTS.asyncify_data_s.rewind_id, 'rewindId', 'i32') }}};
},

#if RELOCATABLE
getDataRewindFunc__deps: [ '$resolveGlobalSymbol' ],
#endif
getDataRewindFunc: function(ptr) {
var id = {{{ makeGetValue('ptr', C_STRUCTS.asyncify_data_s.rewind_id, 'i32') }}};
var name = Asyncify.callStackIdToName[id];
var func = Module['asm'][name];
#if RELOCATABLE
// Exported functions in side modules are not listed in `Module["asm"]`,
// So we should use `resolveGlobalSymbol` helper function, which is defined in `library_dylink.js`.
if (!func) {
func = resolveGlobalSymbol(name, false);
}
#endif
return func;
},

Expand Down
10 changes: 10 additions & 0 deletions src/library_dylink.js
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,9 @@ var LibraryDylink = {
// add new entries to functionsInTableMap
updateTableMap(tableBase, metadata.tableSize);
moduleExports = relocateExports(instance.exports, memoryBase);
#if ASYNCIFY
moduleExports = Asyncify.instrumentWasmExports(moduleExports);
#endif
if (!flags.allowUndefined) {
reportUndefinedSymbols();
}
Expand Down Expand Up @@ -1020,6 +1023,13 @@ var LibraryDylink = {
#if DYLINK_DEBUG
err('dlsym: ' + symbol + ' getting table slot for: ' + result);
#endif

#if ASYNCIFY
// Asyncify wraps exports, and we need to look through those wrappers.
if ('orig' in result) {
result = result.orig;
}
#endif
// Insert the function into the wasm table. If its a direct wasm function
// the second argument will not be needed. If its a JS function we rely
// on the `sig` attribute being set based on the `<func>__sig` specified
Expand Down
64 changes: 64 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3975,6 +3975,40 @@ def test_dlfcn_feature_in_lib(self):
'''
self.do_run(src, 'float: 42.\n')

@needs_dylink
@no_wasm64('TODO: asyncify for wasm64')
def test_dlfcn_asyncify(self):
self.set_setting('ASYNCIFY')

create_file('liblib.c', r'''
#include <stdio.h>
#include <emscripten/emscripten.h>
int side_module_run() {
printf("before sleep\n");
emscripten_sleep(1000);
printf("after sleep\n");
return 42;
}
''')
self.build_dlfcn_lib('liblib.c')

self.prep_dlfcn_main()
src = r'''
#include <stdio.h>
#include <dlfcn.h>
typedef int (*func_t)();
int main(int argc, char **argv) {
void *_dlHandle = dlopen("liblib.so", RTLD_NOW | RTLD_LOCAL);
func_t my_func = (func_t)dlsym(_dlHandle, "side_module_run");
printf("%d\n", my_func());
return 0;
}
'''
self.do_run(src, 'before sleep\nafter sleep\n42\n')

def dylink_test(self, main, side, expected=None, header=None, force_c=False,
main_module=2, **kwargs):
# Same as dylink_testf but take source code in string form
Expand Down Expand Up @@ -8101,6 +8135,36 @@ def test_asyncify_indirect_lists(self, args, should_pass):
if should_pass:
raise

@needs_dylink
@no_wasm64('TODO: asyncify for wasm64')
def test_asyncify_side_module(self):
self.set_setting('ASYNCIFY')
self.set_setting('ASYNCIFY_IMPORTS', ['my_sleep'])
self.dylink_test(r'''
#include <stdio.h>
#include "header.h"
int main() {
printf("before sleep\n");
my_sleep(1);
printf("after sleep\n");
return 0;
}
''', r'''
#include <stdio.h>
#include <emscripten.h>
#include "header.h"
void my_sleep(int milli_seconds) {
// put variable onto stack
volatile int value = 42;
printf("%d\n", value);
emscripten_sleep(milli_seconds);
// variable on stack in side module function should be restored.
printf("%d\n", value);
}
''', 'before sleep\n42\n42\nafter sleep\n', header='void my_sleep(int);', force_c=True)

@no_asan('asyncify stack operations confuse asan')
@no_wasm64('TODO: asyncify for wasm64')
def test_emscripten_scan_registers(self):
Expand Down

0 comments on commit 7f8dfa7

Please sign in to comment.