From 7f8dfa74768ea2ee42723cb5d98d3e2414bd3882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=82=81=E3=81=AE=E3=81=93=E3=81=AB=E3=82=87?= =?UTF-8?q?=E3=81=93=E3=81=AB=E3=82=87=E3=81=93?= Date: Wed, 20 Jul 2022 04:27:21 +0900 Subject: [PATCH] Asyncify: Add support for dynamic linking (#15893) 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. --- emcc.py | 9 ++++ emscripten.py | 3 ++ site/source/docs/porting/asyncify.rst | 43 ++++++++++++++++++ src/library.js | 6 +++ src/library_async.js | 13 ++++++ src/library_dylink.js | 10 +++++ tests/test_core.py | 64 +++++++++++++++++++++++++++ 7 files changed, 148 insertions(+) diff --git a/emcc.py b/emcc.py index f0ef4e95b2664..0e58f0d2aa375 100755 --- a/emcc.py +++ b/emcc.py @@ -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: @@ -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'] diff --git a/emscripten.py b/emscripten.py index f8b5cebd6bf40..5ee929247a5ee 100644 --- a/emscripten.py +++ b/emscripten.py @@ -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'] diff --git a/site/source/docs/porting/asyncify.rst b/site/source/docs/porting/asyncify.rst index c799ad34129ce..cb306f7edd464 100644 --- a/site/source/docs/porting/asyncify.rst +++ b/site/source/docs/porting/asyncify.rst @@ -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 + + 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 + + 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 ################# diff --git a/src/library.js b/src/library.js index 2ebdd0cd8f11e..5f6bd77504d8f 100644 --- a/src/library.js +++ b/src/library.js @@ -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'], diff --git a/src/library_async.js b/src/library_async.js index e64d2c495b06d..8ccffcc33a78d 100644 --- a/src/library_async.js +++ b/src/library_async.js @@ -187,6 +187,9 @@ mergeInto(LibraryManager.library, { } } }; +#if MAIN_MODULE + ret[x].orig = original; +#endif } else { ret[x] = original; } @@ -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; }, diff --git a/src/library_dylink.js b/src/library_dylink.js index 9c786a5f35e2a..9731c02e536ef 100644 --- a/src/library_dylink.js +++ b/src/library_dylink.js @@ -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(); } @@ -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 `__sig` specified diff --git a/tests/test_core.py b/tests/test_core.py index 36f12492e119a..605c395d87fa6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -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 + #include + + 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 + #include + + 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 @@ -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 + #include "header.h" + + int main() { + printf("before sleep\n"); + my_sleep(1); + printf("after sleep\n"); + return 0; + } + ''', r''' + #include + #include + #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):