-
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
[EH] Make abort() work with Wasm EH #16910
Conversation
- `do_run` also sets `expected_output=None`, like `do_runf`. - `do_run`, `do_run_from_file`, and `do_run_in_out_file_test` now return the stdout+stderr output, like `do_runf`.
We used to implement `abort()` with throwing a JS exception. emscripten-core#9730 changed it to `RuntimeError`, because it is the class a trap becomes once it hits a JS frame. But this turned out to be not working, because Wasm EH currently does not assume all `RuntimeError`s are from traps; it decides whether a `RuntimeError` is from a trap or not based on a hidden field within the object. Relevant past discussion: WebAssembly/exception-handling#89 (comment) So at the moment we don't have a way of throwing a trap from JS, which is inconvenient. I think we eventually want to make JS API for this, but for the moment, this PR resorts to a wasm function that traps. This at least makes calling `std::terminate` work. So far calling it exhausted the call stack and aborted. Fixes emscripten-core#16407.
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 there are two different notions of abort()
in emscripten:
- Native code calls
abort()
- JS code calls
abort()
When native code calls abort() we currently map that to JS library function (also called abort() which when calls the JS handler for abort()).
What if we just implemented the native abort() in C and only use the JS abort for JS aborts.. would that work? that sounds like a good idea for code size anyway since it would avoid the extra JS import.
In that JS code that calls abort() would get a different behaviour.. but I think that maybe OK? What exactly is the observable differences between JS throwing and wasm trapping?
.globl __trap | ||
|
||
__trap: | ||
.functype __trap() -> () |
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 you can just write this in C as void __trap() { __builtin_trap() }
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
tests/test_core.py
Outdated
err = self.do_run(r''' | ||
#include <exception> | ||
int main() { | ||
std::terminate(); |
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 just call C abort() here to be more direct?
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 this needs a little more context.
noexcept
function shouldn't throw, so noexcept
function code generation is to invoke
every function call in those functions and in case they throw, call std::terminate
. This codegen comes from clang and native platforms do this too. So in wasm, they become something like
try
function body
catch_all
call std::terminate
end
std::terminate
calls std::__terminate
. Both of std::terminate
and std::__terminate
are noexcept
. So that means their code is structured like that, which sounds like self-calling, but normally no function calls in those functions should ever throw, so that's fine. But in our case, abort
ends up throwing, which is a problem.
The function body of __terminate
eventually calls JS abort
, and ends up here:
Lines 605 to 623 in 970998b
// Use a wasm runtime error, because a JS error might be seen as a foreign | |
// exception, which means we'd run destructors on it. We need the error to | |
// simply make the program stop. | |
// Suppress closure compiler warning here. Closure compiler's builtin extern | |
// defintion for WebAssembly.RuntimeError claims it takes no arguments even | |
// though it can. | |
// TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. | |
/** @suppress {checkTypes} */ | |
var e = new WebAssembly.RuntimeError(what); | |
#if MODULARIZE | |
readyPromiseReject(e); | |
#endif | |
// Throw the error whether or not MODULARIZE is set because abort is used | |
// in code paths apart from instantiation where an exception is expected | |
// to be thrown when abort is called. | |
throw e; |
This ends up throwing a JS exception. This is basically just a foreign exception from the wasm perspective, and is caught by catch_all
, and calls std::terminate
again. And the whole process continues until the call stack is exhausted.
What #9730 tried to do was throwing a trap, because Wasm catch
/catch_all
don't catch traps. Traps become RuntimeError
s after they hit a JS frame. To be consistent, we decided catch
/catch_all
shouldn't catch them after they become RuntimeError
s. That's the reason #9730 changed the code to throw not just any random thing but RuntimeError
. But somehow we decided that we make that trap distinction not based on RuntimeError
class but some hidden field (WebAssembly/exception-handling#89 (comment)).
So to answer your original question, if we use abort()
, it will just throw RuntimeError
and end there without a problem, because there's no noexcept
function involved and there will be no catch_all
and call std::terminate
. We need std::terminate
to show the call stack exhausting behavior.
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 I understand.. Perhaps a more direct test would be that its not possible to catch exceptions thrown by abort()
. e.g.:
try {
abort();
} catch (...) {
print('should never see this\n');
}
Here we are testing that whatever the low level abort does is not catchable.. which I think is important part of this change.
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 is what I wrote and deleted. Actually I was confused and this is not caught by the catch
, because our catch (...)
only catches C++ exceptions for now. I think whether catch (...)
should catch foreign exceptions or not is something we should decide in the tool convention, and current impl is not catching it. So JS exceptions are only caught by the cleanups, which run destructors.
So I think removing noexcept
from std::terminate
and std::__terminate
work; it will unwind the stack and run the destructors, but that's maybe OK with users..? (This is one of the things we discussed in the meeting this morning; whether abort
should unwind or not)
What are the cases that JS code calls
Yeah because one prints something like "exception thrown" and the other prints "unreachable" reached, so the message will be little different. To be precise, the original message:
The message with wasm trap:
It's not ideal, but I think better than the current
|
Think like calling
I wouldn't expect those to be caught by anybody.. but that same is true for all abort calls. They signal the program is not longer running/runnable.
Yeah, I think for debug builds its important to keep the nice messages, but maybe not for release builds.
|
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.
lgtm, looks like the right approach!
Some minor comment suggestions, feel free to accept what you think makes sense.
Separately (I mean, not urgent in this PR), I like @sbc100 's idea to implement abort()
without any JS in release builds, as a simplification&optimization there.
Also separately, since this PR doesn't affect any metadce test expections, I guess we don't have any metadce tests with wasm EH? It might be a good idea to add one, maybe around here:
emscripten/tests/test_other.py
Lines 7340 to 7341 in 5584dd1
'except': (['-O2', '-fexceptions'], [], ['waka']), # noqa | |
# exceptions does not pull in demangling by default, which increases code size |
Hmm, I think I have another simpler approach. Let me upload that first. |
Sorry, nevermind what I wrote and deleted a few minutes ago. |
@@ -0,0 +1,3 @@ | |||
void __trap() { | |||
__builtin_trap(); |
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.
Lets just call this file __trap.c
for now?
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
tests/test_core.py
Outdated
err = self.do_run(r''' | ||
#include <exception> | ||
int main() { | ||
std::terminate(); |
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 I understand.. Perhaps a more direct test would be that its not possible to catch exceptions thrown by abort()
. e.g.:
try {
abort();
} catch (...) {
print('should never see this\n');
}
Here we are testing that whatever the low level abort does is not catchable.. which I think is important part of this change.
Co-authored-by: Alon Zakai <[email protected]>
I think #16921 can be a simpler approach. That version of Like 30 mins ago I posted something along the line of #16910 (comment), but deleted it after that. I was confused; currently |
I see, so then maybe something like then: test_abort_no_cleanup.cpp:
|
#16921 will print "should never print". That's because If we are not OK with the unwinding behavior (#16921), we should stick to this PR I guess. I assumed |
Sorry for the confusion. |
I think that's a separate matter? #16871 was to make a note that it's not working at the moment. Yeah so in that case we need an exception that does not run destructors, which we currently don't have. But isn't that different from In another way, we can solve this by creating a JS tag and making the toolchain to add |
For |
Yeah that's not working now, which is the reason I commented about it in #16871. But that's not |
Done in #16924. |
Are you saying that abort is not working this way even with emscripten-sjlj exceptions? Or are you just talking about with wasm exceptions? |
I thought so too, but I think it was suggested by Dan that
With Emscripten EH / SjLj, everything works fine. With Wasm EH, |
I think that was in the context of At least in emscripten today throw the string "unwind" when we don't want destructors run. |
Then replace "unwinding the stack" with "running destructors" in #16910 (comment) and that's what I meant. I meant running destructors all along. Nevermind unwinding for now. Anyway, as I said, this PR does not run destructors. |
After thinking about this a bit more, IMO it would be better not to have |
Actually, I went and looked up abort and std::terminate in the standards. std::terminate is more interesting than I thought:
My read on that is that it's always OK not to unwind the stack and sometimes required. |
All the seems to be about what to do before one calls |
Yeah, reading it also convinces me that we shouldn't call destructors after all. Thanks for the reference. I guess we can land this and abandon #16921? |
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.
lgtm % comments
tools/system_libs.py
Outdated
@@ -708,7 +708,8 @@ class libcompiler_rt(MTLibrary, SjLjLibrary): | |||
'stack_ops.S', | |||
'stack_limits.S', | |||
'emscripten_setjmp.c', | |||
'emscripten_exception_builtins.c' | |||
'emscripten_exception_builtins.c', | |||
'__trap.c' |
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.
Add a trailing comma.. to make future additions cleaner.
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
tests/test_core.py
Outdated
@@ -1639,6 +1639,20 @@ class Polymorphic {virtual void member(){}}; | |||
} | |||
''', 'exception caught: std::bad_typeid') | |||
|
|||
@with_both_eh_sjlj | |||
def test_terminate_abort(self): |
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.
How about renaming this "test_abort_no_dtors" and rewriting it to call C abort
, with a C++ object on the stack?
The reason I ask is that this change seem more about the behaviour of abort
than the behaviour of the higher level std::terminate
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
@@ -621,6 +629,7 @@ function abort(what) { | |||
// in code paths apart from instantiation where an exception is expected | |||
// to be thrown when abort is called. | |||
throw 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.
I think it might make sense to delete this path and just use __trap
in all cases.. but I guess that can be a followup maybe?
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.
Yeah I didn't do that in this PR because
- This changes the error message slightly for existing users, which I don't think is a big deal, but just to be conservative
- I eventually want to revive this, by making JS API that can create traps; so I was somewhat hesitant to delete this part.
I don't think either of these is an important enough though. WDYT?
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'm OK with landing this now and then looking at possible simplification later.
I think I'm currently a fan of (in release builds) avoiding the JS import of abort completely.. and keeping this fulling native where possible.
When landing emscripten-core#16910, I didn't rebase it onto emscripten-core#16924 so they raced in the commit CI. This reflects the changes in emscripten-core#16910.
We decided use a trap for `abort` function in case of Wasm EH in order to prevent infinite-looping (emscripten-core#16910). https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L472-L474 The short reason is, in Wasm EH `RuntimeError` is treated as a foreign exception and caught by `catch_all`, so you try to abort the program and throw a `RuntimeError`, but it is caught by some `catch_all` used in the cleanup (=destructors, etc) code, causing the program to continue to run. `__trap` is defined in compiler-rt and exported when Wasm EH is enabled. This has worked so far, but in case we fail to instantiate a Wasm module and call `abort` because of it, we don't have access to the imported `Module['asm']['__trap']`, because `Module['asm']` has not been set: https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L848 So the `abort` call will ends up like this: ```console ... TypeError: Cannot read properties of undefined (reading '__trap') at ___trap (/usr/local/google/home/aheejin/test/gl84/exported_api.js:5152:34) at abort (/usr/local/google/home/aheejin/test/gl84/exported_api.js:892:5) at /usr/local/google/home/aheejin/test/gl84/exported_api.js:1172:5 ``` which may not be the worst thing in the world given that we are crashing anyway, but not the situation we intended. This PR lets us throw `RuntimeError` in case we don't have the wasm module instantiated and have access to `Module['asm']['__trap']`. This is OK even with Wasm EH because we haven't been running the module, there's no risk of infinite-looping we tried to prevent in emscripten-core#16910. I'm not sure how to add a test for this, because the test would require a module that fails to instantiate. This was found while I was debugging something else.
We decided use a trap for `abort` function in case of Wasm EH in order to prevent infinite-looping (emscripten-core#16910). https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L472-L474 The short reason is, in Wasm EH `RuntimeError` is treated as a foreign exception and caught by `catch_all`, so you try to abort the program and throw a `RuntimeError`, but it is caught by some `catch_all` used in the cleanup (=destructors, etc) code, causing the program to continue to run. `__trap` is defined in compiler-rt and exported when Wasm EH is enabled. This has worked so far, but in case we fail to instantiate a Wasm module and call `abort` because of it, we don't have access to the imported `Module['asm']['__trap']`, because `Module['asm']` has not been set: https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L848 So the `abort` call will ends like this: ```console TypeError: Cannot read properties of undefined (reading '__trap') at ___trap (/usr/local/google/home/aheejin/test/gl84/exported_api.js:5152:34) at abort (/usr/local/google/home/aheejin/test/gl84/exported_api.js:892:5) at /usr/local/google/home/aheejin/test/gl84/exported_api.js:1172:5 ``` which may not be the worst thing in the world given that we are crashing anyway, but not the situation we intended. This PR lets us throw `RuntimeError` in case we don't have the wasm module instantiated and have access to `Module['asm']['__trap']`. This is OK even with Wasm EH because we haven't been running the module, there's no risk of infinite-looping we tried to prevent in emscripten-core#16910. I'm not sure how to add a test for this, because the test would require a module that fails to instantiate. This was found while I was debugging something else.
We decided use a trap for `abort` function in case of Wasm EH in order to prevent infinite-looping (emscripten-core#16910). https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L472-L474 The short reason is, in Wasm EH `RuntimeError` is treated as a foreign exception and caught by `catch_all`, so you try to abort the program and throw a `RuntimeError`, but it is caught by some `catch_all` used in the cleanup (=destructors, etc) code, causing the program to continue to run. `__trap` is defined in compiler-rt and exported when Wasm EH is enabled. This has worked so far, but in case we fail to instantiate a Wasm module and call `abort` because of it, we don't have access to the imported `Module['asm']['__trap']`, because `Module['asm']` has not been set: https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L848 So the `abort` call will end like this: ```console TypeError: Cannot read properties of undefined (reading '__trap') at ___trap (/usr/local/google/home/aheejin/test/gl84/exported_api.js:5152:34) at abort (/usr/local/google/home/aheejin/test/gl84/exported_api.js:892:5) at /usr/local/google/home/aheejin/test/gl84/exported_api.js:1172:5 ``` which may not be the worst thing in the world given that we are crashing anyway, but not the situation we intended. This PR lets us throw `RuntimeError` in case we don't have the wasm module instantiated and have access to `Module['asm']['__trap']`. This is OK even with Wasm EH because we haven't been running the module, there's no risk of infinite-looping we tried to prevent in emscripten-core#16910. I'm not sure how to add a test for this, because the test would require a module that fails to instantiate. This was found while I was debugging something else.
We decided use a trap for `abort` function in case of Wasm EH in order to prevent infinite-looping (#16910). https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L472-L474 The short reason is, in Wasm EH `RuntimeError` is treated as a foreign exception and caught by `catch_all`, so you try to abort the program and throw a `RuntimeError`, but it is caught by some `catch_all` used in the cleanup (=destructors, etc) code, causing the program to continue to run. `__trap` is defined in compiler-rt and exported when Wasm EH is enabled. This has worked so far, but in case we fail to instantiate a Wasm module and call `abort` because of it, we don't have access to the imported `Module['asm']['__trap']`, because `Module['asm']` has not been set: https://github.com/emscripten-core/emscripten/blob/44b2c2a1ecad39c534e14179acb49419dfee528b/src/preamble.js#L848 So the `abort` call will end like this: ```console TypeError: Cannot read properties of undefined (reading '__trap') at ___trap (/usr/local/google/home/aheejin/test/gl84/exported_api.js:5152:34) at abort (/usr/local/google/home/aheejin/test/gl84/exported_api.js:892:5) at /usr/local/google/home/aheejin/test/gl84/exported_api.js:1172:5 ``` which may not be the worst thing in the world given that we are crashing anyway, but not the situation we intended. This PR lets us throw `RuntimeError` in case we don't have the wasm module instantiated and have access to `Module['asm']['__trap']`. This is OK even with Wasm EH because we haven't been running the module, there's no risk of infinite-looping we tried to prevent in #16910.
We used to implement
abort()
with throwing a JS exception. #9730changed it to
RuntimeError
, because it is the class a trap becomesonce it hits a JS frame.
But this turned out to be not working, because Wasm EH currently does
not assume all
RuntimeError
s are from traps; it decides whether aRuntimeError
is from a trap or not based on a hidden field within theobject. Relevant past discussion:
WebAssembly/exception-handling#89 (comment)
So at the moment we don't have a way of throwing a trap from JS, which
is inconvenient. I think we eventually want to make JS API for this, but
for the moment, this PR resorts to a wasm function that traps. This at
least makes calling
std::terminate
work. So far calling it exhaustedthe call stack and aborted.
Fixes #16407.