Skip to content

[EH] Make std::terminate() work with EH #16921

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

Closed
wants to merge 4 commits into from

Conversation

aheejin
Copy link
Member

@aheejin aheejin commented May 10, 2022

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 now. 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:

emscripten/src/preamble.js

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 RuntimeErrors
after they hit a JS frame. To be consistent, we decided
catch/catch_all shouldn't catch them after they become
RuntimeErrors. 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)).

This PR removes noexcept from std::terminate and
std::__terminate's signatures so that the cleanup that contains
catch_all is not generated for those two functions. So now the JS
exception thrown by abort() will unwind the stack, which is different
from native, but that can be considered OK because I don't think users
expect abort to preserve the stack intact?

Fixes #16407.

`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
```wasm
try
  function body
catch_all
  call std::terminate
end
```

`std::terminate` calls `std::__terminate`. Both of `std::terminate` and
`std::__terminate` are `noexcept` now. 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:
https://github.com/emscripten-core/emscripten/blob/970998b2670a9bcf39d31e2b01db571089955add/src/preamble.js#L605-L623

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 emscripten-core#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 emscripten-core#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)).

This PR removes `noexcept` from `std::terminate` and
`std::__terminate`'s signatures so that the cleanup that contains
`catch_all` is not generated for those two functions. So now the JS
exception thrown by `abort()` will unwind the stack, which is different
from native, but that can be considered OK because I don't think users
expect `abort` to preserve the stack intact?

Fixes emscripten-core#16407.
@aheejin
Copy link
Member Author

aheejin commented May 10, 2022

This is an alternative approach to #16910.

Copy link
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

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

Some questions:

  1. Are there downsides to removing noexcept? I'd guess it prevents some LLVM optimizations, maybe mostly in LTO, but probably [[noreturn]] is good enough anyhow?
  2. Can we get rid of the try-catch for noexcept functions in release builds? I understand from you that they appear in native builds too. But it seems like they handle an error case by converting one type of error (an exception thrown where one shouldn't be) into another (a terminate), and I'd hope that a release build would prefer to not add overhead for that. I guess in theory an exception could be thrown that is caught by the application higher up, so the try-catch converts potentially incorrect internal operations into a clear immediate terminate, but still, that feels odd to me in a release build - it feels like assertions used in debug builds.

@@ -45,7 +45,11 @@ unexpected_handler get_unexpected() noexcept;
typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler f ) noexcept;
terminate_handler get_terminate() noexcept;
#ifdef __USING_WASM_EXCEPTIONS__
Copy link
Member

Choose a reason for hiding this comment

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

Might want to add a comment here, or just point people to libcxxabi for an explanation. Unless that's obvious enough?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was not sure if it would be OK to copy-paste the same comment four times. Did that anyway.

@aheejin
Copy link
Member Author

aheejin commented May 10, 2022

Some questions:

  1. Are there downsides to removing noexcept? I'd guess it prevents some LLVM optimizations, maybe mostly in LTO, but probably [[noreturn]] is good enough anyhow?

I don't think it would harm optimizations. The frontend doesn't think std::terminate throws anyway, which I think is a baked-in assumption.

  1. Can we get rid of the try-catch for noexcept functions in release builds? I understand from you that they appear in native builds too. But it seems like they handle an error case by converting one type of error (an exception thrown where one shouldn't be) into another (a terminate), and I'd hope that a release build would prefer to not add overhead for that. I guess in theory an exception could be thrown that is caught by the application higher up, so the try-catch converts potentially incorrect internal operations into a clear immediate terminate, but still, that feels odd to me in a release build - it feels like assertions used in debug builds.

But that will make behavior different. std::terminate might be OK because it is a special case, but if we do that for all noexcept user functions,

void foo() noexcept {
  throw 3;
}

int main() {
  try {
    foo();
  } catch (...) {
    printf("I shouldn't be printed");
  }
}

"I shouldn't be printed" will not be printed in debug builds but will be printed in release builds.


But I became more skeptical about this approach; maybe we shouldn't run destructors after abort after all and land #16910.

@kripken
Copy link
Member

kripken commented May 10, 2022

"I shouldn't be printed" will not be printed in debug builds but will be printed in release builds.

Oh, right, this is more general than terminate... Yes, my suggestion wasn't useful then.

I lean towards #16910 too, but I don't have a strong preference.

@aheejin
Copy link
Member Author

aheejin commented May 11, 2022

Closing in favor of #16910.

@aheejin aheejin closed this May 11, 2022
@aheejin aheejin deleted the terminate2 branch May 11, 2022 00:01
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.

Stack overflow when calling std::terminate + -fwasm-exceptions
2 participants