From 96d7b56ed30ea42bf861b602006afe678b9f4ea4 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 19 Feb 2026 00:15:09 +0000 Subject: [PATCH 1/5] [embind] Manage exceptions' states correctly `libemval.js` has two exception-related functions: `_emval_throw` and `_emval_from_current_cxa_exception`, but they haven't been correclty updating various exception-related states. #26276 tried to fix this but it only updated `exceptionLast`. This updates other internal states correctly. Some of them only apply to Emscripten EH, while others apply to both. 1. To make the uncaught exception count updatable for Wasm EH, this adds `___cxa_in/decrement_uncaught_exception` to `cxa_exception_js_utils.cpp`, and also adds a common interface `in/decrementUncaughtExceptionCount` in `libexceptions.js` so that you can call them regardless of which EH is used. For Wasm EH, the management of the uncaught exception count is done within libc++abi functions (`__cxa_throw`, `__cxa_rethrow`, ...), but here in embind we bypass those functions, so we have to manage them directly. 2. To make refcount work correctly, this adds calls to `in/decrementExceptionRefcount` to exception-related functions. Also, this fixes memory leaks by adding the destructor mechanism that decrements refcount when emvals are destroyed. `_emval_from_current_cxa_exception` is similar to `std::current_exception`, which returns `std::exception_ptr`, whose destructor decrements the refcount so that the exception can be destroyed: https://github.com/emscripten-core/emscripten/blob/62e22652509fbe7a00609ce48a653d0d66f27ba5/system/lib/libcxx/src/support/runtime/exception_pointer_cxxabi.ipp#L16 embind didn't have such a mechanism, so exceptions captured by `_emval_from_current_cxa_exception` leaked memory. This also fixes #26290. --- src/lib/libemval.js | 86 +++++++++++++------ src/lib/libexceptions.js | 20 +++++ src/lib/libsigs.js | 2 + .../libcxxabi/src/cxa_exception_js_utils.cpp | 11 +++ ...embind_throw_val_uncaught_and_refcount.cpp | 47 ++++++++++ ...embind_throw_val_uncaught_and_refcount.out | 10 +++ test/test_core.py | 4 + 7 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 test/embind/test_embind_throw_val_uncaught_and_refcount.cpp create mode 100644 test/embind/test_embind_throw_val_uncaught_and_refcount.out diff --git a/src/lib/libemval.js b/src/lib/libemval.js index f2e1b9e42bf07..3a9eaf2c37913 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..462c76aa5019f 100644 --- a/src/lib/libexceptions.js +++ b/src/lib/libexceptions.js @@ -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); @@ -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/src/lib/libsigs.js b/src/lib/libsigs.js index bb990b3f9c142..5a9e5f6628547 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -222,6 +222,8 @@ sigs = { __cxa_current_exception_type__sig: 'p', __cxa_current_primary_exception__sig: 'p', __cxa_end_catch__sig: 'v', + __cxa_decrement_uncaught_exception__sig: 'v', + __cxa_increment_uncaught_exception__sig: 'v', __cxa_rethrow__sig: 'v', __cxa_rethrow_primary_exception__sig: 'vp', __cxa_throw__sig: 'vppp', 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/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), From c83c7a0538ace0c246629f2e3c87caadc40731cd Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Sun, 22 Mar 2026 19:24:26 +0000 Subject: [PATCH 2/5] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (2) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_minimal_runtime_code_size_hello_embind.json: 14909 => 14961 [+52 bytes / +0.35%] codesize/test_minimal_runtime_code_size_hello_embind_val.json: 11642 => 11695 [+53 bytes / +0.46%] Average change: +0.40% (+0.35% - +0.46%) ``` --- .../test_minimal_runtime_code_size_hello_embind.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_embind_val.json | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 } From 2b321531ddf5eb34f3e6be52a69176d9a17f8adb Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Sun, 22 Mar 2026 19:31:00 +0000 Subject: [PATCH 3/5] Make several Emscripten EH JS function available at DISABLE_EXCEPTION_THROWING --- src/lib/libexceptions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/libexceptions.js b/src/lib/libexceptions.js index 462c76aa5019f..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(); @@ -369,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) => { From 0172b722f171c7151de049a0ee208ded3f3d1a81 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 23 Mar 2026 01:23:13 +0000 Subject: [PATCH 4/5] _emval_is_cpp_exception -> $emval_is_cpp_exception --- src/lib/libemval.js | 8 ++++---- src/lib/libsigs.js | 2 -- system/lib/libcxxabi/include/cxxabi.h | 3 +++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/lib/libemval.js b/src/lib/libemval.js index 3a9eaf2c37913..eb236709a24c3 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -399,8 +399,8 @@ ${functionBody} return delete object[property]; }, - _emval_is_cpp_exception__deps: ['$Emval'], - _emval_is_cpp_exception: (object) => { + $emval_is_cpp_exception__deps: ['$Emval'], + $emval_is_cpp_exception: (object) => { object = Emval.toValue(object); #if WASM_EXCEPTIONS return object instanceof WebAssembly.Exception; @@ -413,7 +413,7 @@ ${functionBody} #endif }, - _emval_throw__deps: ['$Emval', '_emval_is_cpp_exception', + _emval_throw__deps: ['$Emval', '$emval_is_cpp_exception', #if !DISABLE_EXCEPTION_THROWING && !WASM_EXCEPTIONS '$exceptionLast', '$ExceptionInfo', @@ -429,7 +429,7 @@ ${functionBody} _emval_throw: (object) => { var orig_object = object; object = Emval.toValue(object); - if (__emval_is_cpp_exception(orig_object)) { + if (emval_is_cpp_exception(orig_object)) { #if !DISABLE_EXCEPTION_THROWING && !WASM_EXCEPTIONS exceptionLast = object; var info = new ExceptionInfo(exnToPtr(object)); diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 5a9e5f6628547..bb990b3f9c142 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -222,8 +222,6 @@ sigs = { __cxa_current_exception_type__sig: 'p', __cxa_current_primary_exception__sig: 'p', __cxa_end_catch__sig: 'v', - __cxa_decrement_uncaught_exception__sig: 'v', - __cxa_increment_uncaught_exception__sig: 'v', __cxa_rethrow__sig: 'v', __cxa_rethrow_primary_exception__sig: 'vp', __cxa_throw__sig: 'vppp', diff --git a/system/lib/libcxxabi/include/cxxabi.h b/system/lib/libcxxabi/include/cxxabi.h index 9ea93680f62e0..fc950de558eb6 100644 --- a/system/lib/libcxxabi/include/cxxabi.h +++ b/system/lib/libcxxabi/include/cxxabi.h @@ -176,6 +176,9 @@ __cxa_increment_exception_refcount(void *primary_exception) _LIBCXXABI_NOEXCEPT; extern _LIBCXXABI_FUNC_VIS void __cxa_decrement_exception_refcount(void *primary_exception) _LIBCXXABI_NOEXCEPT; +extern _LIBCXXABI_FUNC_VIS void __cxa_increment_uncaught_exception() _LIBCXXABI_NOEXCEPT; +extern _LIBCXXABI_FUNC_VIS void __cxa_decrement_uncaught_exception() _LIBCXXABI_NOEXCEPT; + // Apple extension to support std::uncaught_exception() extern _LIBCXXABI_FUNC_VIS bool __cxa_uncaught_exception() _LIBCXXABI_NOEXCEPT; extern _LIBCXXABI_FUNC_VIS unsigned int __cxa_uncaught_exceptions() _LIBCXXABI_NOEXCEPT; From ccd80940d770bc528b08a803b59a6dbedc1452ef Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 23 Mar 2026 01:50:56 +0000 Subject: [PATCH 5/5] Revert unnecessary changes --- system/lib/libcxxabi/include/cxxabi.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/system/lib/libcxxabi/include/cxxabi.h b/system/lib/libcxxabi/include/cxxabi.h index fc950de558eb6..9ea93680f62e0 100644 --- a/system/lib/libcxxabi/include/cxxabi.h +++ b/system/lib/libcxxabi/include/cxxabi.h @@ -176,9 +176,6 @@ __cxa_increment_exception_refcount(void *primary_exception) _LIBCXXABI_NOEXCEPT; extern _LIBCXXABI_FUNC_VIS void __cxa_decrement_exception_refcount(void *primary_exception) _LIBCXXABI_NOEXCEPT; -extern _LIBCXXABI_FUNC_VIS void __cxa_increment_uncaught_exception() _LIBCXXABI_NOEXCEPT; -extern _LIBCXXABI_FUNC_VIS void __cxa_decrement_uncaught_exception() _LIBCXXABI_NOEXCEPT; - // Apple extension to support std::uncaught_exception() extern _LIBCXXABI_FUNC_VIS bool __cxa_uncaught_exception() _LIBCXXABI_NOEXCEPT; extern _LIBCXXABI_FUNC_VIS unsigned int __cxa_uncaught_exceptions() _LIBCXXABI_NOEXCEPT;