diff --git a/ChangeLog.md b/ChangeLog.md index ba1444a863979..a80a6ec0ee331 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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 ----------------- diff --git a/embuilder.py b/embuilder.py index 1aab1d1316023..51eb7fbfdf10e 100755 --- a/embuilder.py +++ b/embuilder.py @@ -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', @@ -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', diff --git a/emcc.py b/emcc.py index 08b962f1566bb..9479dac09eec5 100755 --- a/emcc.py +++ b/emcc.py @@ -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 diff --git a/src/library_exceptions.js b/src/library_exceptions.js index 686d0e8a5cebc..511030bab2791 100644 --- a/src/library_exceptions.js +++ b/src/library_exceptions.js @@ -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 + // 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 diff --git a/system/lib/libcxxabi/src/cxa_exception.cpp b/system/lib/libcxxabi/src/cxa_exception.cpp index a92997b9031f9..bbb6cd8ddbe4c 100644 --- a/system/lib/libcxxabi/src/cxa_exception.cpp +++ b/system/lib/libcxxabi/src/cxa_exception.cpp @@ -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 @@ -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 diff --git a/test/test_other.py b/test/test_other.py index cfeef55022fea..b6a3391a82d9b 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -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) diff --git a/tools/system_libs.py b/tools/system_libs.py index c9f80bb28811c..52a885b50f1cb 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -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', @@ -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: