diff --git a/src/lib/libemval.js b/src/lib/libemval.js index f2e1b9e42bf07..eb236709a24c3 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -11,6 +11,7 @@ var LibraryEmVal = { // Stack of handles available for reuse. $emval_freelist: [], + $emval_destructors: [], // Array of alternating pairs (value, refcount). // reserve 0 and some special values. These never get de-allocated. $emval_handles: [ @@ -80,13 +81,19 @@ var LibraryEmVal = { } }, - _emval_decref__deps: ['$emval_freelist', '$emval_handles'], + _emval_decref__deps: ['$emval_freelist', '$emval_handles', '$emval_destructors'], _emval_decref: (handle) => { if (handle > {{{ EMVAL_LAST_RESERVED_HANDLE }}} && 0 === --emval_handles[handle + 1]) { #if ASSERTIONS assert(emval_handles[handle] !== undefined, `Decref for unallocated handle.`); #endif + var value = emval_handles[handle]; emval_handles[handle] = undefined; + var destructor = emval_destructors[handle]; + if (destructor) { + emval_destructors[handle] = undefined; + destructor(value); + } emval_freelist.push(handle); } }, @@ -392,37 +399,50 @@ ${functionBody} return delete object[property]; }, - _emval_throw__deps: ['$Emval', -#if !WASM_EXCEPTIONS + $emval_is_cpp_exception__deps: ['$Emval'], + $emval_is_cpp_exception: (object) => { + object = Emval.toValue(object); +#if WASM_EXCEPTIONS + return object instanceof WebAssembly.Exception; +#else +#if EXCEPTION_STACK_TRACES + return object instanceof CppException; +#else + return object === object+0; // Check if it is a number +#endif +#endif + }, + + _emval_throw__deps: ['$Emval', '$emval_is_cpp_exception', +#if !DISABLE_EXCEPTION_THROWING && !WASM_EXCEPTIONS '$exceptionLast', + '$ExceptionInfo', + '$exnToPtr', +#endif +#if !DISABLE_EXCEPTION_THROWING || WASM_EXCEPTIONS + '$incrementUncaughtExceptionCount', +#endif +#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS + '$incrementExceptionRefcount', #endif ], _emval_throw: (object) => { + var orig_object = object; object = Emval.toValue(object); -#if !WASM_EXCEPTIONS - // If we are throwing Emcripten C++ exception, set exceptionLast, as we do - // in __cxa_throw. When EXCEPTION_STACK_TRACES is set, a C++ exception will - // be an instance of EmscriptenEH, and when EXCEPTION_STACK_TRACES is not - // set, it will be a pointer (number). - // - // This is different from __cxa_throw() in libexception.js because - // __cxa_throw() is called from the user C++ code when the 'throw' keyword - // is used, and the value thrown is a C++ pointer. When - // EXCEPTION_STACK_TRACES is true, we wrap it with CppException. But this - // _emval_throw is called when we throw whatever is contained in 'object', - // which can be anything including a CppException object, or a number, or - // other JS object. So we don't use storeException() wrapper here and we - // throw it as is. -#if EXCEPTION_STACK_TRACES - if (object instanceof CppException) { - exceptionLast = object; - } -#else - if (object === object+0) { // Check if it is a number + if (emval_is_cpp_exception(orig_object)) { +#if !DISABLE_EXCEPTION_THROWING && !WASM_EXCEPTIONS exceptionLast = object; - } + var info = new ExceptionInfo(exnToPtr(object)); + info.set_caught(false); + info.set_rethrown(false); +#endif +#if !DISABLE_EXCEPTION_THROWING || WASM_EXCEPTIONS + incrementUncaughtExceptionCount(); #endif +#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS + incrementExceptionRefcount(object); #endif + } throw object; }, @@ -463,7 +483,12 @@ ${functionBody} })); }, - _emval_from_current_cxa_exception__deps: ['$Emval', '__cxa_rethrow'], + _emval_from_current_cxa_exception__deps: ['$Emval', '__cxa_rethrow', '$emval_destructors', +#if !DISABLE_EXCEPTION_THROWING || WASM_EXCEPTIONS + '$decrementUncaughtExceptionCount', + '$decrementExceptionRefcount', +#endif + ], _emval_from_current_cxa_exception: () => { try { // Use __cxa_rethrow which already has mechanism for generating @@ -472,7 +497,16 @@ ${functionBody} // with metadata optimised out otherwise. ___cxa_rethrow(); } catch (e) { - return Emval.toHandle(e); +#if !DISABLE_EXCEPTION_THROWING || WASM_EXCEPTIONS + // ___cxa_rethrow incremented uncaughtExceptionCount. + // Since we caught it in JS, we need to manually decrement it to balance. + decrementUncaughtExceptionCount(); +#endif + var handle = Emval.toHandle(e); +#if !DISABLE_EXCEPTION_THROWING || WASM_EXCEPTIONS + emval_destructors[handle] = decrementExceptionRefcount; +#endif + return handle; } }, }; diff --git a/src/lib/libexceptions.js b/src/lib/libexceptions.js index fd84b2e8114b6..368d21a57461f 100644 --- a/src/lib/libexceptions.js +++ b/src/lib/libexceptions.js @@ -284,7 +284,7 @@ var LibraryExceptions = { }, #endif -#if WASM_EXCEPTIONS || !DISABLE_EXCEPTION_CATCHING +#if WASM_EXCEPTIONS || !DISABLE_EXCEPTION_THROWING $getExceptionMessageCommon__deps: ['__get_exception_message', 'free', '$stackSave', '$stackRestore', '$stackAlloc'], $getExceptionMessageCommon: (ptr) => { var sp = stackSave(); @@ -341,6 +341,16 @@ var LibraryExceptions = { return ___thrown_object_from_unwind_exception(unwind_header); }, + $incrementUncaughtExceptionCount__deps: ['__cxa_increment_uncaught_exception'], + $incrementUncaughtExceptionCount: () => { + ___cxa_increment_uncaught_exception(); + }, + + $decrementUncaughtExceptionCount__deps: ['__cxa_decrement_uncaught_exception'], + $decrementUncaughtExceptionCount: () => { + ___cxa_decrement_uncaught_exception(); + }, + $incrementExceptionRefcount__deps: ['__cxa_increment_exception_refcount', '$getCppExceptionThrownObjectFromWebAssemblyException'], $incrementExceptionRefcount: (ex) => { var ptr = getCppExceptionThrownObjectFromWebAssemblyException(ex); @@ -359,7 +369,7 @@ var LibraryExceptions = { return getExceptionMessageCommon(ptr); }, -#elif !DISABLE_EXCEPTION_CATCHING +#elif !DISABLE_EXCEPTION_THROWING // When EXCEPTION_STACK_TRACES is set, the exception is an instance of // CppException, whereas EXCEPTION_STACK_TRACES is unset it is a raw pointer. $exnToPtr: (exn) => { @@ -371,6 +381,16 @@ var LibraryExceptions = { return exn; }, + $incrementUncaughtExceptionCount__deps: ['$uncaughtExceptionCount'], + $incrementUncaughtExceptionCount: () => { + uncaughtExceptionCount++; + }, + + $decrementUncaughtExceptionCount__deps: ['$uncaughtExceptionCount'], + $decrementUncaughtExceptionCount: () => { + uncaughtExceptionCount--; + }, + $incrementExceptionRefcount__deps: ['$exnToPtr', '__cxa_increment_exception_refcount'], $incrementExceptionRefcount: (exn) => ___cxa_increment_exception_refcount(exnToPtr(exn)), diff --git a/system/lib/libcxxabi/src/cxa_exception_js_utils.cpp b/system/lib/libcxxabi/src/cxa_exception_js_utils.cpp index 05d908d45577d..a01629e8ddbf4 100644 --- a/system/lib/libcxxabi/src/cxa_exception_js_utils.cpp +++ b/system/lib/libcxxabi/src/cxa_exception_js_utils.cpp @@ -123,6 +123,17 @@ char* __get_exception_terminate_message(void* thrown_object) { free(type); return result; } + +#ifdef __WASM_EXCEPTIONS__ +void __cxa_increment_uncaught_exception() throw() { + __cxa_get_globals()->uncaughtExceptions += 1; +} + +void __cxa_decrement_uncaught_exception() throw() { + __cxa_get_globals()->uncaughtExceptions -= 1; +} +#endif + } // extern "C" } // namespace __cxxabiv1 diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind.json b/test/codesize/test_minimal_runtime_code_size_hello_embind.json index 6f8026cfbadb0..a76340ae3eb55 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind.json @@ -1,10 +1,10 @@ { "a.html": 548, "a.html.gz": 371, - "a.js": 7262, - "a.js.gz": 3324, + "a.js": 7314, + "a.js.gz": 3347, "a.wasm": 7099, "a.wasm.gz": 3257, - "total": 14909, - "total_gz": 6952 + "total": 14961, + "total_gz": 6975 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json index 1140b98f22baa..7126e080feb41 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json @@ -1,10 +1,10 @@ { "a.html": 548, "a.html.gz": 371, - "a.js": 5353, - "a.js.gz": 2524, + "a.js": 5406, + "a.js.gz": 2544, "a.wasm": 5741, "a.wasm.gz": 2690, - "total": 11642, - "total_gz": 5585 + "total": 11695, + "total_gz": 5605 } diff --git a/test/embind/test_embind_throw_val_uncaught_and_refcount.cpp b/test/embind/test_embind_throw_val_uncaught_and_refcount.cpp new file mode 100644 index 0000000000000..c4e3baa0c88ec --- /dev/null +++ b/test/embind/test_embind_throw_val_uncaught_and_refcount.cpp @@ -0,0 +1,47 @@ +#include +#include +#include + +using namespace emscripten; + +int main() { + val error = val::null(); + try { + throw std::runtime_error("test exception"); + } catch (const std::exception& e) { + // Capture the exception + error = val::take_ownership( + emscripten::internal::_emval_from_current_cxa_exception()); + } + + std::cout << "Captured exception." << std::endl; + + int uncaught_before = std::uncaught_exceptions(); + std::cout << "Uncaught before throw 1: " << uncaught_before << std::endl; + + // First throw + try { + std::cout << "Throwing 1..." << std::endl; + error.throw_(); + } catch (const std::exception& e) { + std::cout << "Caught 1: " << e.what() << std::endl; + int uncaught_during = std::uncaught_exceptions(); + std::cout << "Uncaught during catch 1: " << uncaught_during << std::endl; + } + + int uncaught_between = std::uncaught_exceptions(); + std::cout << "Uncaught between throws: " << uncaught_between << std::endl; + + // Second throw - if refcount was messed up, this might fail/crash + try { + std::cout << "Throwing 2..." << std::endl; + error.throw_(); + } catch (const std::exception& e) { + std::cout << "Caught 2: " << e.what() << std::endl; + int uncaught_during = std::uncaught_exceptions(); + std::cout << "Uncaught during catch 2: " << uncaught_during << std::endl; + } + + std::cout << "Done." << std::endl; + return 0; +} diff --git a/test/embind/test_embind_throw_val_uncaught_and_refcount.out b/test/embind/test_embind_throw_val_uncaught_and_refcount.out new file mode 100644 index 0000000000000..eb2c80ecbc4e3 --- /dev/null +++ b/test/embind/test_embind_throw_val_uncaught_and_refcount.out @@ -0,0 +1,10 @@ +Captured exception. +Uncaught before throw 1: 0 +Throwing 1... +Caught 1: test exception +Uncaught during catch 1: 0 +Uncaught between throws: 0 +Throwing 2... +Caught 2: test exception +Uncaught during catch 2: 0 +Done. diff --git a/test/test_core.py b/test/test_core.py index 7f2766fb1464e..9976eec64e153 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -7778,6 +7778,10 @@ def test_embind_wasm_workers(self): def test_embind_throw_cpp_exception(self): self.do_run_in_out_file_test('embind/test_embind_throw_cpp_exception.cpp', cflags=['-lembind', '-std=c++20']) + @with_all_eh_sjlj + def test_embind_throw_val_uncaught_and_refcount(self): + self.do_run_in_out_file_test('embind/test_embind_throw_val_uncaught_and_refcount.cpp', cflags=['-lembind', '-std=c++20']) + @parameterized({ '': ('DEFAULT', False), 'all': ('ALL', False),