Skip to content

[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

Merged
merged 14 commits into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ See docs/process.md for more on how version tagging works.

3.1.24 (in development)
-----------------------
- In Wasm exception mode (`-fwasm-exceptions`), when `ASSERTIONS` is enabled,
uncaught exceptions will display stack traces. (#17979)

3.1.23 - 09/23/22
-----------------
Expand Down
5 changes: 5 additions & 0 deletions embuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
'libc++abi',
'libc++abi-except',
'libc++abi-noexcept',
'libc++abi-debug',
'libc++abi-debug-except',
'libc++abi-debug-noexcept',
'libc++',
'libc++-except',
'libc++-noexcept',
Expand Down Expand Up @@ -76,6 +79,8 @@
'libc_optz-mt-debug',
'libc++abi-mt',
'libc++abi-mt-noexcept',
'libc++abi-debug-mt',
'libc++abi-debug-mt-noexcept',
'libc++-mt',
'libc++-mt-noexcept',
'libdlmalloc-mt',
Expand Down
5 changes: 5 additions & 0 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,11 @@ def get_full_import_name(name):
if settings.WASM_EXCEPTIONS:
settings.REQUIRED_EXPORTS += ['__trap']

# When ASSERTIONS is set, we include stack traces in Wasm exception objects
# using the JS API, which needs this C++ tag exported.
if settings.ASSERTIONS and settings.WASM_EXCEPTIONS:
settings.EXPORTED_FUNCTIONS += ['___cpp_exception']

# Make `getExceptionMessage` and other necessary functions available for use.
if settings.EXPORT_EXCEPTION_HANDLING_HELPERS:
# We also export refcount increasing and decreasing functions because if you
Expand Down
36 changes: 33 additions & 3 deletions src/library_exceptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,14 +414,44 @@ var LibraryExceptions = {
#endif
#if WASM_EXCEPTIONS
$getCppExceptionTag: function() {
// In static linking, tags are defined within the wasm module and are
// exported, whereas in dynamic linking, tags are defined in library.js in
// JS code and wasm modules import them.
#if RELOCATABLE
return ___cpp_exception; // defined in library.js
#else
return Module['asm']['__cpp_exception'];
#endif
},

#if ASSERTIONS
// Throw a WebAssembly.Exception object with the C++ tag with a stack trace
// embedded. 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
Copy link
Collaborator

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"

Copy link
Member

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.)

Copy link
Member Author

Choose a reason for hiding this comment

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

@sbc100 Added the comment.
@kripken Put the #if ASSERTIONS guard around it.

// In release builds, this function is not needed and the native
// _Unwind_RaiseException in libunwind is used instead.
__throw_exception_with_stack_trace__deps: ['$getCppExceptionTag'],
__throw_exception_with_stack_trace: function(ex) {
var e = new WebAssembly.Exception(getCppExceptionTag(), [ex], {traceStack: true});
// The generated stack trace will be in the form of:
//
// Error
// at ___throw_exception_with_stack_trace(test.js:1139:13)
// at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367)
// ...
//
// Remove this JS function name, which is in the second line, from the stack
// trace.
var arr = e.stack.split('\n');
arr.splice(1,1);
e.stack = arr.join('\n');
throw e;
},
#endif

// Given an WebAssembly.Exception object, returns the actual user-thrown
// C++ object address in the Wasm memory.
// 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
$getCppExceptionThrownObjectFromWebAssemblyException__deps: ['$getCppExceptionTag', '__thrown_object_from_unwind_exception'],
$getCppExceptionThrownObjectFromWebAssemblyException: function(ex) {
// In Wasm EH, the value extracted from WebAssembly.Exception is a pointer
Expand Down
15 changes: 15 additions & 0 deletions system/lib/libcxxabi/src/cxa_exception.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ handler, _Unwind_RaiseException may return. In that case, __cxa_throw
will call terminate, assuming that there was no handler for the
exception.
*/

#if defined(__USING_WASM_EXCEPTIONS__) && !defined(NDEBUG)
extern "C" {
void __throw_exception_with_stack_trace(_Unwind_Exception*, bool);
} // extern "C"
#endif

void
#ifdef __USING_WASM_EXCEPTIONS__
// In wasm, destructors return their argument
Expand Down Expand Up @@ -280,6 +287,14 @@ __cxa_throw(void *thrown_object, std::type_info *tinfo, void (*dest)(void *)) {

#ifdef __USING_SJLJ_EXCEPTIONS__
_Unwind_SjLj_RaiseException(&exception_header->unwindHeader);
#elif __USING_WASM_EXCEPTIONS__
#ifdef NDEBUG
_Unwind_RaiseException(&exception_header->unwindHeader);
#else
// In debug mode, call a JS library function to use WebAssembly.Exception JS
// API, which enables us to include stack traces
__throw_exception_with_stack_trace(&exception_header->unwindHeader, true);
#endif
#else
_Unwind_RaiseException(&exception_header->unwindHeader);
#endif
Expand Down
40 changes: 40 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -7904,6 +7904,46 @@ def test_wasm_nope(self):
out = self.run_js('a.out.js', assert_returncode=NON_ZERO)
self.assertContained('no native wasm support detected', out)

@requires_v8
def test_wasm_exceptions_stack_trace(self):
src = r'''
void bar() {
throw 3;
}
void foo() {
bar();
}
int main() {
foo();
return 0;
}
'''
emcc_args = ['-g', '-fwasm-exceptions']
self.v8_args.append('--experimental-wasm-eh')

# Stack trace example for this example code:
# exiting due to exception: [object WebAssembly.Exception],Error
# 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)
stack_trace_checks = ['at __cxa_throw', 'at bar', 'at foo', 'at main']

# We attach stack traces to exception objects only when ASSERTIONS is set
self.set_setting('ASSERTIONS')
self.do_run(src, emcc_args=emcc_args, assert_all=True,
assert_returncode=NON_ZERO, expected_output=stack_trace_checks)

self.set_setting('ASSERTIONS', 0)
err = self.do_run(src, emcc_args=emcc_args, assert_returncode=NON_ZERO)
for check in stack_trace_checks:
self.assertNotContained(check, err)

@requires_node
def test_jsrun(self):
print(config.NODE_JS)
Expand Down
3 changes: 1 addition & 2 deletions tools/system_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1350,7 +1350,7 @@ def can_use(self):
return super().can_use() and settings.SHARED_MEMORY


class libcxxabi(NoExceptLibrary, MTLibrary):
class libcxxabi(NoExceptLibrary, MTLibrary, DebugLibrary):
name = 'libc++abi'
cflags = [
'-Oz',
Expand All @@ -1363,7 +1363,6 @@ class libcxxabi(NoExceptLibrary, MTLibrary):

def get_cflags(self):
cflags = super().get_cflags()
cflags.append('-DNDEBUG')
if not self.is_mt and not self.is_ww:
cflags.append('-D_LIBCXXABI_HAS_NO_THREADS')
if self.eh_mode == Exceptions.NONE:
Expand Down