Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle wildcards in ASYNCIFY_EXPORTS when intrumenting exports #19298

Merged

Conversation

mboc-qt
Copy link
Contributor

@mboc-qt mboc-qt commented May 5, 2023

Binaryen already handles wildcards sent to it via jspi-exports, which is derived from ASYNCIFY_EXPORTS. Sync that with instrumentWasmExports, which (until now) has been ignoring the wildcards.

@mboc-qt
Copy link
Contributor Author

mboc-qt commented May 5, 2023

I couldn't find a suitable test for this. Should I write a new one? Where should that be, test_other, test_browser, somewhere else?

@kripken
Copy link
Member

kripken commented May 5, 2023

For a test, maybe in browser, and near the one modified in #19280?

@mboc-qt mboc-qt force-pushed the async-instrument-exports-wildcard/1 branch from e3183d2 to 6cc81e7 Compare May 11, 2023 09:59
@mboc-qt
Copy link
Contributor Author

mboc-qt commented May 11, 2023

Test created

@mboc-qt
Copy link
Contributor Author

mboc-qt commented May 11, 2023

Tests flaked I think... ?

@mboc-qt mboc-qt force-pushed the async-instrument-exports-wildcard/1 branch from 6cc81e7 to 6604615 Compare May 12, 2023 16:20
@@ -110,7 +110,7 @@ mergeInto(LibraryManager.library, {
dbg('asyncify instrumenting exports');
#endif
#if ASYNCIFY == 2
var ASYNCIFY_EXPORTS = {{{ JSON.stringify(ASYNCIFY_EXPORTS) }}};
var exportPatterns = [{{{ ASYNCIFY_EXPORTS.map(x => new RegExp('^' + x.replace(new RegExp('\\*', 'g'), '.*') + '$')) }}}];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the outer new RegExp.. you can just use / at the beginning and end, no? Like in the other locations where we do this?

window.disableErrorReporting = true;
// For some reason, e does not contain an error in case of bad export instrumentation.
// Send the result manually here.
window.onerror = function(e) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems rather hacky.. i would hope we could find a better way? But its hard for me to say without debugging the the code why this would be needed?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test replaced

# self.skipTest(f'Current browser ({EMTEST_BROWSER}) does not support JSPI. Only chromium-based browsers ({CHROMIUM_BASED_BROWSERS}) support JSPI today.')
# TODO(mboc-qt): Use the above once browser_test_add_opera/1 is merged.
self.skipTest(f'Current browser ({EMTEST_BROWSER}) does not support JSPI.')
self.btest('browser/async_exports.cpp', '0', args=['-sASYNCIFY=2', '-Wno-experimental'] + args + ['-sASSERTIONS'])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a browser test? Can it go in test_other.py or test_core.py? @brendandahl WDYT? Does this require a completely new test or can we modify an existing one?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_other.py is probably more appropriate (see test_embind_jspi for an example).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried moving it there, but see my other comment below - no easy way to check if a function has been instrumented or not.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to test_other with help from @brendandahl

}

extern "C" EMSCRIPTEN_KEEPALIVE void otherUnsuspendExport(int value) {
unsuspend(value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just make this file a .c file and then you don't need the extern "C" here.

Also the indention here looks wrong. 2 space indents everywhere is the convention I think.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

# self.skipTest(f'Current browser ({EMTEST_BROWSER}) does not support JSPI. Only chromium-based browsers ({CHROMIUM_BASED_BROWSERS}) support JSPI today.')
# TODO(mboc-qt): Use the above once browser_test_add_opera/1 is merged.
self.skipTest(f'Current browser ({EMTEST_BROWSER}) does not support JSPI.')
self.btest('browser/async_exports.cpp', '0', args=['-sASYNCIFY=2', '-Wno-experimental'] + args + ['-sASSERTIONS'])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use btest_exit rather than btest? Then you don't need the REPORT_RESULT macro at all. Its preferable in most cases.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -0,0 +1,72 @@
// Copyright 2023 The Emscripten Authors. All rights reserved.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test can be simplified a bit, we have other tests for more of the functionality. The main thing we need to test is if the exports get instrumented. How about:

  1. export three c functions
  2. have the wild card match 2
  3. in an EM_ASYNC_JScall the 2 that should match and assert that they return promises. Also, call the third and assert it returns a value.

Copy link
Contributor Author

@mboc-qt mboc-qt May 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that instrumenting functions does not necessarily make them return promises.
Taking a look at instrumentWasmExports in library_async.js, instrumenting just changes the function to one that calls Asyncify.exportCallStack.push/pop (ASYNCIFY=1) or wraps it with one that has o promising parameter (ASYNCIFY=2) - so there is no easy way to check by simply verifying if the return value is a promise.
Also, if we wanted to check if the promising parameter has been added, there's also no easy way of doing this in a test, because we have no access to the original unwrapped WASM function, since it is wrapped in a js function in instrumentWasmExports.
We could check that in case of a MAIN_MODULE, because then we have access to export.orig (see ret[x].orig = original; in library_async.js), but I don't know if that would be a correct approach for a unittest.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're only testing ASYNCIFY=2 so the instrumented function should always return a promise. I was thinking something like:

#include <emscripten.h>

EM_ASYNC_JS(int, test, (), {
  console.log(Module._async1() instanceof Promise);
  console.log(Module._async2() instanceof Promise);
  console.log(!(Module._sync() instanceof Promise));
});

EMSCRIPTEN_KEEPALIVE int async1() {
  return 99;
}

EMSCRIPTEN_KEEPALIVE int async2() {
  return 99;
}

EMSCRIPTEN_KEEPALIVE int sync() {
  return 99;
}

int main() {
  test();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I did - but the problem is that instrumenting alone does not make the function return a promise automatically.
See library_async.js' Asyncify.instrumentWasmImports:
the only thing it does is add the first parameter of 'externref' and adding the suspending: 'first' parameter to WebAssembly.Function. The return type from the call to sigToWasmTypes is left intact.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you seeing the code not actually return a promise? There were older versions of v8 where it didn't always return promise, but newer versions should always return a promise. The return type will be what the promise resolves with.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to test the instrumentation itself, just that it has been instrumented. If it wasn't instrumented it will not return a promise.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I actually wanted to say; instrumenting does not make it return a promise, and adding emscripten_sleep(0); will always make it return a promise, regardless of instrumentation.
Since all we want here is to check if functions were instrumented, and a promise being returned is not a result of instrumentation, I cannot think of any viable way of testing this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's what I had in mind. This test passes with the PR and fails without the PR.

diff --git a/test/other/jspi_wildcard.c b/test/other/jspi_wildcard.c
new file mode 100644
index 000000000..e4e07afa4
--- /dev/null
+++ b/test/other/jspi_wildcard.c
@@ -0,0 +1,31 @@
+#include <emscripten.h>
+#include <stdio.h>
+
+EM_ASYNC_JS(int, test, (), {
+  let promise1 = Module._async1();
+  assert(promise1 instanceof Promise);
+  await promise1;
+  let promise2 = Module._async2();
+  assert(promise2 instanceof Promise);
+  await promise2;
+  assert(!(Module._sync() instanceof Promise));
+});
+
+EMSCRIPTEN_KEEPALIVE int async1() {
+  emscripten_sleep(0);
+  return 99;
+}
+
+EMSCRIPTEN_KEEPALIVE int async2() {
+  emscripten_sleep(0);
+  return 99;
+}
+
+EMSCRIPTEN_KEEPALIVE int sync() {
+  return 99;
+}
+
+int main() {
+  test();
+  printf("done\n");
+}
diff --git a/test/test_other.py b/test/test_other.py
index d8e1916d2..6853a0bdf 100644
--- a/test/test_other.py
+++ b/test/test_other.py
@@ -2868,6 +2868,13 @@ int f() {
     self.assertContained('Constructed from JS destructed', output)
     self.assertNotContained('Foo* destructed', output)
 
+  def test_jspi_wildcard(self):
+    self.require_v8()
+    self.v8_args.append('--experimental-wasm-stack-switching')
+    self.emcc_args += ['-g', '-sASYNCIFY=2', '-sASYNCIFY_EXPORTS=async*', '-Wno-experimental']
+
+    self.do_runf(test_file('other/jspi_wildcard.c'), 'done')
+
   def test_emconfig(self):
     output = self.run_process([emconfig, 'LLVM_ROOT'], stdout=PIPE).stdout.strip()
     self.assertEqual(output, config.LLVM_ROOT)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yes - it follows the same logic as my first test. As long as we assume that we are okay with not asserting on the exact error message:

caught (in promise) RuntimeError: invalid suspender object for suspend
at emscripten_sleep (main.wasm:0x4aa1)
at async1 (test_jspi_wildcard.c:20:3)
at export$async1 (main.wasm:0x4a7c)
at ret. (main.js:1404:35)
at Object._async1 (main.js:730:22)
at main.js:1017:96
at main.js:1424:11
at main.js:1415:11

then I'm okay with that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(test pushed, only slightly changed)

@mboc-qt mboc-qt force-pushed the async-instrument-exports-wildcard/1 branch from 6604615 to fe7bf9d Compare June 12, 2023 08:41
Binaryen already handles wildcards sent to it via jspi-exports,
which is derived from ASYNCIFY_EXPORTS. Sync that with
instrumentWasmExports, which, until now, has been ignoring the wildcards.

Add new test 'other.test_jspi_wildcard' verifying that it works.
@mboc-qt mboc-qt force-pushed the async-instrument-exports-wildcard/1 branch from fe7bf9d to 2bd6dc3 Compare June 12, 2023 10:48
@brendandahl brendandahl merged commit 9052744 into emscripten-core:main Jun 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants