-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[EH] Support stack traces for Wasm exceptions #17979
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
Conversation
This embeds stack traces into `WebAssembly.Exception` objects when `ASSERTION` is set. To do this, we now have a separate debug version of libc++abi, whose `__cxa_throw` doesn't call libunwind's `_Unwind_RaiseException`, which uses Wasm's `throw` instruction directly to throw exceptions, but rather calls out to a helper JS function that creates and throws `WebAssembly.Exception`. That JS function uses the optional `stack` property of `WebAssembly.Exception` constructor to attach stack traces to objects. Without `ASSERTION` set, when an exception is thrown and uncaught, we only see something like ``` exiting due to exception: [object WebAssembly.Exception] ``` And this is what we've seen so far as well. With this patch, when `ASSERTION` is set, we see stack traces like ``` exiting due to exception: [object WebAssembly.Exception],Error at ___throwCppWebAssemblyException (test.js:1139:13) at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) at bar() (wasm://wasm/009a7c9a:wasm-function[12]:0xf53) at foo() (wasm://wasm/009a7c9a:wasm-function[19]:0x154e) at __original_main (wasm://wasm/009a7c9a:wasm-function[20]:0x15a6) at main (wasm://wasm/009a7c9a:wasm-function[56]:0x25be) at test.js:833:22 at callMain (test.js:4567:15) at doRun (test.js:4621:23) at run (test.js:4636:5) ``` Fixes emscripten-core#17466.
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.
Nice!
#else | ||
// In debug mode, call a JS library function to use WebAssembly.Exception JS | ||
// API, which enables us to include stack traces | ||
__throwCppWebAssemblyException(&exception_header->unwindHeader, true); |
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.
What about calling this _Unwind_RaiseException_WithStacktrace
? Since its basically the same function as _Unwind_RaiseException
? Maybe it doesn't need a second argument if its always called with true?
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.
_Unwind_
functions are the naming convention of libunwind, and this function is in library_exceptions.js
.. So I named it like this. On the other hand, library_exception.js
's (or other library JS files') naming schemes are kind of mixed... Some are camelCase, some are snake_case, and some are with two underscores, some are with one underscore, some are without... So it was kind of not very easy to name it. Should it have leading underscores or not?
The reason I added the boolean option is in case users want to directly use that function to throw WebAssembly.Exception
from JS, in which case they may or may not want the stack traces.
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.
But we don't have such users yet do we? So maybe we can keep the code simple for now and make it more complex later if it proves useful, and also keep this function private/internal.
Also, a user who doesn't want a stacktrace could/should presumably just call the existing _Unwind_RaiseException
function.
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.
Hmm, given that we decided to use this as an internal function for the moment, will rename this to __throw_exception_with_stack_trace
. (Not sure about JS library naming schemes, but it looks internal functions are likely to be in snake_case...) Let me know if you have a better idea.
#else | ||
// In debug mode, call a JS library function to use WebAssembly.Exception JS | ||
// API, which enables us to include stack traces | ||
__throwCppWebAssemblyException(&exception_header->unwindHeader, true); |
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.
But we don't have such users yet do we? So maybe we can keep the code simple for now and make it more complex later if it proves useful, and also keep this function private/internal.
Also, a user who doesn't want a stacktrace could/should presumably just call the existing _Unwind_RaiseException
function.
// Given an WebAssembly.Exception object, returns the actual user-thrown | ||
// C++ object address in the Wasm memory. | ||
// Throw a WebAssembly.Exception object with the C++ tag. If traceStack is | ||
// true, includes the stack traces within the exception object. | ||
// WebAssembly.Exception is a JS object representing a Wasm exception, | ||
// provided by Wasm JS API: | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Exception |
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.
Perhaps add something like. "In release builds this function is not needed and the native _Unwind_RaiseException
is used instead"
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.
To add to that, perhaps we can put the entire function within #if ASSERTIONS
? That would both further clarify it is for debug builds, and also we'd get a link error if we try to use it by mistake in the future.
(Though from discussion below perhaps we want to allow this to be used optionally in release builds too. In that case ignore my comment here.)
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.
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.
Nice!
lgtm % ongoing discussions
// Given an WebAssembly.Exception object, returns the actual user-thrown | ||
// C++ object address in the Wasm memory. | ||
// Throw a WebAssembly.Exception object with the C++ tag. If traceStack is | ||
// true, includes the stack traces within the exception object. | ||
// WebAssembly.Exception is a JS object representing a Wasm exception, | ||
// provided by Wasm JS API: | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Exception |
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.
To add to that, perhaps we can put the entire function within #if ASSERTIONS
? That would both further clarify it is for debug builds, and also we'd get a link error if we try to use it by mistake in the future.
(Though from discussion below perhaps we want to allow this to be used optionally in release builds too. In that case ignore my comment here.)
The docs here are worth updating after this: (doesn't need to be in this PR) |
In two-phase unwinding, the first phase is the search phase (`_UA_SEARCH_PHASE`) and the second one is the cleanup phase (`_UA_CLEANUP_PHASE`). The search phase searches up the stack to see if there is a matching catch handler, and if it finds one, it caches the result. And in the second cleanup phase, it retrieves the cached result (to avoid doing same work twice) and unwinds the stack. Wasm does not do the two-phase unwinding; it only has a single phase. We used `_UA_CLEANUP_PHASE` for this single phase, so in Wasm the cleanup phase is supposed to the search. So we several many custom `#ifdef`s to use the code guarded by `_UA_SEARCH_PHASE`, for example: https://github.com/aheejin/emscripten/blob/d57db5bea1719319a680699c50b91fa3d88fa0ec/system/lib/libcxxabi/src/cxa_personality.cpp#L771-L776 https://github.com/aheejin/emscripten/blob/d57db5bea1719319a680699c50b91fa3d88fa0ec/system/lib/libcxxabi/src/cxa_personality.cpp#L850-L855 These are apparently gone in emscripten-core#14288, which replaced many `if`s with `assert`s. This in effect removed our special handling for `_UA_CLEANUP_PHASE`; there are several `assert`s that asserts the current phase is `_UA_SEARCH_PHASE`, while Wasm is in `_UA_CLEANUP_PHASE`. But this has not caused problems so far because we have built libc++abi with `-NDEBUG`, so all assertions were no-op. https://github.com/emscripten-core/emscripten/blob/40fb7d2071e439f1de614898b88518df582faa94/tools/system_libs.py#L1366 But this is now a problem because emscripten-core#17979 adds a debug build of libc++abi, which enables assertions. Come to think of it, I'm not sure why I decided to use `_UA_CLEANUP_PHASE` for our single phase in the first place. If we use `_UA_SEARCH_PHASE`, we can remove more our custom code and reduce the difference between our port and the upstream library.
In two-phase unwinding, the first phase is the search phase (`_UA_SEARCH_PHASE`) and the second one is the cleanup phase (`_UA_CLEANUP_PHASE`). The search phase searches up the stack to see if there is a matching catch handler, and if it finds one, it caches the result. And in the second cleanup phase, it retrieves the cached result (to avoid doing same work twice) and unwinds the stack. Wasm does not do the two-phase unwinding; it only has a single phase. We used `_UA_CLEANUP_PHASE` for this single phase, so in Wasm the cleanup phase is supposed to the search. So we several many custom `#ifdef`s to use the code guarded by `_UA_SEARCH_PHASE`, for example: https://github.com/aheejin/emscripten/blob/d57db5bea1719319a680699c50b91fa3d88fa0ec/system/lib/libcxxabi/src/cxa_personality.cpp#L771-L776 https://github.com/aheejin/emscripten/blob/d57db5bea1719319a680699c50b91fa3d88fa0ec/system/lib/libcxxabi/src/cxa_personality.cpp#L850-L855 These are apparently gone in #14288, which replaced many `if`s with `assert`s. This in effect removed our special handling for `_UA_CLEANUP_PHASE`; there are several `assert`s that asserts the current phase is `_UA_SEARCH_PHASE`, while Wasm is in `_UA_CLEANUP_PHASE`. But this has not caused problems so far because we have built libc++abi with `-NDEBUG`, so all assertions were no-op. https://github.com/emscripten-core/emscripten/blob/40fb7d2071e439f1de614898b88518df582faa94/tools/system_libs.py#L1366 But this is now a problem because #17979 adds a debug build of libc++abi, which enables assertions. Come to think of it, I'm not sure why I decided to use `_UA_CLEANUP_PHASE` for our single phase in the first place. If we use `_UA_SEARCH_PHASE`, we can remove more our custom code and reduce the difference between our port and the upstream library.
After emscripten-core#17979, when `ASSERTIONS` is set, uncaught exceptions carry stack traces that are printed to the screen for debugging help. This adds an exception message to the exception objects in addition to that. The message it adds is produced by `__get_exception_message` function: https://github.com/emscripten-core/emscripten/blob/f6c46570e3780e52050bf822a07b342ec4bdddbe/system/lib/libcxxabi/src/cxa_exception_emscripten.cpp#L75-L111 If an exception is a subclass of `std::exception`, it returns `std::exception::what()`. If not, it just prints its type. If an exception is uncaught and its type is a subclass of `std::exception`, now we print the `what()` message along with the stack trace when `ASSERTION` is set. In case our exception is `std::runtime_error` and the message is "my exception", when uncaught, this prints: ``` exiting due to exception: [object WebAssembly.Exception],Error: std::runtime_error,my exception at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) ... ``` Fixes emscripten-core#6330.
After #17979, when `ASSERTIONS` is set, uncaught exceptions carry stack traces that are printed to the screen for debugging help. This adds an exception message to the exception objects in addition to that. The message it adds is produced by `__get_exception_message` function: https://github.com/emscripten-core/emscripten/blob/f6c46570e3780e52050bf822a07b342ec4bdddbe/system/lib/libcxxabi/src/cxa_exception_emscripten.cpp#L75-L111 If an exception is a subclass of `std::exception`, it returns `std::exception::what()`. If not, it just prints its type. If an exception is uncaught and its type is a subclass of `std::exception`, now we print the `what()` message along with the stack trace when `ASSERTIONS` is set. In case our exception is `std::runtime_error` and the message is "my exception", when uncaught, this prints: ``` exiting due to exception: [object WebAssembly.Exception],Error: std::runtime_error,my exception at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) ... ``` Fixes #6330.
This embeds stack traces into
WebAssembly.Exception
objects whenASSERTIONS
is set. To do this, we now have a separate debug version of libc++abi, whose__cxa_throw
doesn't call libunwind's_Unwind_RaiseException
, which uses Wasm'sthrow
instruction directly to throw exceptions, but rather calls out to a helper JS function that creates and throwsWebAssembly.Exception
. That JS function uses the optionalstack
property ofWebAssembly.Exception
constructor to attach stack traces to objects.Without
ASSERTIONS
set, when an exception is thrown and uncaught, we only see something likeexiting due to exception: [object WebAssembly.Exception]
And this is what we've seen so far as well.
With this patch, when
ASSERTIONS
is set, we see stack traces likeFixes #17466.