-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Handle wildcards in ASYNCIFY_EXPORTS when intrumenting exports #19298
Conversation
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? |
For a test, maybe in |
e3183d2
to
6cc81e7
Compare
Test created |
Tests flaked I think... ? |
6cc81e7
to
6604615
Compare
src/library_async.js
Outdated
@@ -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'), '.*') + '$')) }}}]; |
There was a problem hiding this comment.
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?
test/browser/async_exports.cpp
Outdated
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test replaced
test/test_browser.py
Outdated
# 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']) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
test/browser/async_exports.cpp
Outdated
} | ||
|
||
extern "C" EMSCRIPTEN_KEEPALIVE void otherUnsuspendExport(int value) { | ||
unsuspend(value); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
test/test_browser.py
Outdated
# 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']) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
test/browser/async_exports.cpp
Outdated
@@ -0,0 +1,72 @@ | |||
// Copyright 2023 The Emscripten Authors. All rights reserved. |
There was a problem hiding this comment.
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:
- export three c functions
- have the wild card match 2
- in an
EM_ASYNC_JS
call the 2 that should match and assert that they return promises. Also, call the third and assert it returns a value.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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();
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
6604615
to
fe7bf9d
Compare
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.
fe7bf9d
to
2bd6dc3
Compare
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.