From 10ae3403b915bcc63d89beaeb9dfdab9f6cc4b1c Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 23 Mar 2026 11:14:11 -0700 Subject: [PATCH] [EmscriptenEH] Always use custom JS class for C/C++ exceptions This has a slight codesize cost but it means we no longer have to worry about the ambiguity in when we catch a Number. --- src/lib/libcore.js | 4 -- src/lib/libdylink.js | 7 --- src/lib/libemval.js | 23 +------- src/lib/libexceptions.js | 58 ++++++++++++------- src/parseTools.mjs | 28 +++++---- src/preamble.js | 6 +- src/runtime_exceptions.js | 18 +++++- test/codesize/test_codesize_cxx_ctors1.json | 8 +-- test/codesize/test_codesize_cxx_ctors2.json | 8 +-- test/codesize/test_codesize_cxx_except.json | 8 +-- test/codesize/test_codesize_cxx_mangle.json | 8 +-- test/codesize/test_codesize_cxx_noexcept.json | 8 +-- test/codesize/test_codesize_cxx_wasmfs.json | 8 +-- .../test_codesize_file_preload.expected.js | 5 ++ test/codesize/test_codesize_hello_dylink.json | 8 +-- .../test_codesize_hello_dylink_all.json | 4 +- .../test_codesize_minimal_O0.expected.js | 5 ++ test/codesize/test_unoptimized_code_size.json | 16 ++--- tools/js_manipulation.py | 10 +--- 19 files changed, 118 insertions(+), 122 deletions(-) diff --git a/src/lib/libcore.js b/src/lib/libcore.js index f690ed0f907b2..d9516f1477ea5 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -484,11 +484,7 @@ addToLibrary({ // a proxy and declare the dependency here. _emscripten_throw_longjmp__deps: ['setThrew'], _emscripten_throw_longjmp: () => { -#if EXCEPTION_STACK_TRACES throw new EmscriptenSjLj; -#else - throw Infinity; -#endif }, #elif !SUPPORT_LONGJMP #if !INCLUDE_FULL_LIBRARY diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 5cb86ee25dd64..ab8ba1dd5b293 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -93,16 +93,9 @@ var LibraryDylink = { } catch(e) { stackRestore(sp); // Create a try-catch guard that rethrows the Emscripten EH exception. -#if EXCEPTION_STACK_TRACES // Exceptions thrown from C++ and longjmps will be an instance of // EmscriptenEH. if (!(e instanceof EmscriptenEH)) throw e; -#else - // Exceptions thrown from C++ will be a pointer (number) and longjmp - // will throw the number Infinity. Use the compact and fast "e !== e+0" - // test to check if e was not a Number. - if (e !== e+0) throw e; -#endif _setThrew(1, 0); #if WASM_BIGINT // In theory this if statement could be done on diff --git a/src/lib/libemval.js b/src/lib/libemval.js index f2e1b9e42bf07..b77744023d311 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -393,35 +393,18 @@ ${functionBody} }, _emval_throw__deps: ['$Emval', -#if !WASM_EXCEPTIONS +#if !WASM_EXCEPTIONS && !DISABLE_EXCEPTION_CATCHING '$exceptionLast', #endif ], _emval_throw: (object) => { object = Emval.toValue(object); -#if !WASM_EXCEPTIONS +#if !WASM_EXCEPTIONS && !DISABLE_EXCEPTION_CATCHING // 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 + // in __cxa_throw. C++ exception will be an instance of CppEmscripten. if (object instanceof CppException) { exceptionLast = object; } -#else - if (object === object+0) { // Check if it is a number - exceptionLast = object; - } -#endif #endif throw object; }, diff --git a/src/lib/libexceptions.js b/src/lib/libexceptions.js index fd84b2e8114b6..1799f96a9ec9c 100644 --- a/src/lib/libexceptions.js +++ b/src/lib/libexceptions.js @@ -7,7 +7,9 @@ var LibraryExceptions = { #if !WASM_EXCEPTIONS $uncaughtExceptionCount: '0', - $exceptionLast: '0', +#if !DISABLE_EXCEPTION_CATCHING + $exceptionLast: null, +#endif $exceptionCaught: ' []', // This class is the exception metadata which is prepended to each thrown object (in WASM memory). @@ -82,14 +84,15 @@ var LibraryExceptions = { // Here, we throw an exception after recording a couple of values that we need to remember // We also remember that it was the last exception thrown as we need to know that later. - __cxa_throw__deps: ['$ExceptionInfo', '$exceptionLast', '$uncaughtExceptionCount', + __cxa_throw__deps: ['$ExceptionInfo', '$uncaughtExceptionCount', #if !DISABLE_EXCEPTION_CATCHING + '$exceptionLast', '__cxa_increment_exception_refcount', #endif #if EXCEPTION_STACK_TRACES - // When EXCEPTION_STACK_TRACES is enabled, storeException contains a call to - // 'new CppException', whose constructor calls getExceptionMessage. We can't - // track the dependency there, so we track it here. + // When EXCEPTION_STACK_TRACES is enabled, the 'CppException' constructor + // calls getExceptionMessage. We can't track the dependency there, so we + // track it here. '$getExceptionMessage', // These functions can be necessary to prevent memory leaks from the JS // side. Even though they are not used it here directly, we export them when @@ -106,17 +109,18 @@ var LibraryExceptions = { info.init(type, destructor); #if !DISABLE_EXCEPTION_CATCHING ___cxa_increment_exception_refcount(ptr); + exceptionLast = new CppException(ptr); #endif - {{{ storeException('exceptionLast', 'ptr') }}} uncaughtExceptionCount++; - {{{ makeThrow('exceptionLast') }}} + {{{ makeThrow() }}} }, // This exception will be caught twice, but while begin_catch runs twice, // we early-exit from end_catch when the exception has been rethrown, so // pop that here from the caught exceptions. - __cxa_rethrow__deps: ['$exceptionCaught', '$exceptionLast', '$uncaughtExceptionCount', + __cxa_rethrow__deps: ['$exceptionCaught', '$uncaughtExceptionCount', #if !DISABLE_EXCEPTION_CATCHING + '$exceptionLast', '__cxa_increment_exception_refcount', #endif ], @@ -135,13 +139,13 @@ var LibraryExceptions = { } #if !DISABLE_EXCEPTION_CATCHING ___cxa_increment_exception_refcount(ptr); -#endif #if EXCEPTION_DEBUG dbg('__cxa_rethrow, popped ' + [ptrToString(ptr), exceptionLast, 'stack', exceptionCaught]); #endif - {{{ storeException('exceptionLast', 'ptr') }}} - {{{ makeThrow('exceptionLast') }}} + exceptionLast = new CppException(ptr); +#endif + {{{ makeThrow() }}} }, llvm_eh_typeid_for: (type) => type, @@ -166,7 +170,11 @@ var LibraryExceptions = { // and free the exception. Note that if the dynCall on the destructor fails // due to calling apply on undefined, that means that the destructor is // an invalid index into the FUNCTION_TABLE, so something has gone wrong. - __cxa_end_catch__deps: ['$exceptionCaught', '$exceptionLast', '__cxa_decrement_exception_refcount', 'setThrew'], + __cxa_end_catch__deps: ['$exceptionCaught', '__cxa_decrement_exception_refcount', 'setThrew', +#if !DISABLE_EXCEPTION_CATCHING + '$exceptionLast', +#endif + ], __cxa_end_catch: () => { // Clear state flag. _setThrew(0, 0); @@ -177,10 +185,12 @@ var LibraryExceptions = { var info = exceptionCaught.pop(); #if EXCEPTION_DEBUG - dbg('__cxa_end_catch popped ' + [info, exceptionLast, 'stack', exceptionCaught]); + dbg('__cxa_end_catch popped ' + [info, 'stack', exceptionCaught]); #endif ___cxa_decrement_exception_refcount(info.excPtr); - exceptionLast = 0; // XXX in decRef? +#if !DISABLE_EXCEPTION_CATCHING + exceptionLast = null; // XXX in decRef? +#endif }, __cxa_uncaught_exceptions__deps: ['$uncaughtExceptionCount'], @@ -224,14 +234,15 @@ var LibraryExceptions = { // unwinding using 'if' blocks around each function, so the remaining // functionality boils down to picking a suitable 'catch' block. // We'll do that here, instead, to keep things simpler. +#if !DISABLE_EXCEPTION_CATCHING $findMatchingCatch__deps: ['$exceptionLast', '$ExceptionInfo', '__cxa_can_catch', '$setTempRet0'], +#endif $findMatchingCatch: (args) => { - var thrown = -#if EXCEPTION_STACK_TRACES - exceptionLast?.excPtr; +#if DISABLE_EXCEPTION_CATCHING + setTempRet0(0); + return 0; #else - exceptionLast; -#endif + var thrown = exceptionLast?.excPtr; if (!thrown) { // just pass through the null ptr setTempRet0(0); @@ -270,17 +281,22 @@ var LibraryExceptions = { } setTempRet0(thrownType); return thrown; +#endif }, +#if !DISABLE_EXCEPTION_CATCHING __resumeException__deps: ['$exceptionLast'], +#endif __resumeException: (ptr) => { +#if !DISABLE_EXCEPTION_CATCHING #if EXCEPTION_DEBUG dbg("__resumeException " + [ptrToString(ptr), exceptionLast]); #endif if (!exceptionLast) { - {{{ storeException('exceptionLast', 'ptr') }}} + exceptionLast = new CppException(ptr); } - {{{ makeThrow('exceptionLast') }}} +#endif + {{{ makeThrow() }}} }, #endif diff --git a/src/parseTools.mjs b/src/parseTools.mjs index b5c11288f31fa..b1560c6dd9d42 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -648,22 +648,21 @@ export function makeReturn64(value) { return `(setTempRet0(${pair[1]}), ${pair[0]})`; } -function makeThrow(excPtr) { - if (ASSERTIONS && DISABLE_EXCEPTION_CATCHING) { - var assertInfo = - 'Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.'; - if (MAIN_MODULE) { - assertInfo += - ' (note: in dynamic linking, if a side module wants exceptions, the main module must be built with that support)'; +function makeThrow() { + if (DISABLE_EXCEPTION_CATCHING) { + if (ASSERTIONS) { + var assertInfo = + 'Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.'; + if (MAIN_MODULE) { + assertInfo += + ' (note: in dynamic linking, if a side module wants exceptions, the main module must be built with that support)'; + } + return `assert(false, '${assertInfo}');`; + } else { + return 'abort()'; } - return `assert(false, '${assertInfo}');`; } - return `throw ${excPtr};`; -} - -function storeException(varName, excPtr) { - var exceptionToStore = EXCEPTION_STACK_TRACES ? `new CppException(${excPtr})` : `${excPtr}`; - return `${varName} = ${exceptionToStore};`; + return 'throw exceptionLast;'; } function charCode(char) { @@ -1255,7 +1254,6 @@ addToCompileTimeContext({ runtimeKeepalivePop, runtimeKeepalivePush, splitI64, - storeException, to64, toIndexType, nodePthreadDetection, diff --git a/src/preamble.js b/src/preamble.js index cbc818e952f3d..2b247c7ee2eaf 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -360,11 +360,7 @@ function makeAbortWrapper(original) { ABORT // rethrow exception if abort() was called in the original function call above || abortWrapperDepth > 1 // rethrow exceptions not caught at the top level if exception catching is enabled; rethrow from exceptions from within callMain #if SUPPORT_LONGJMP == 'emscripten' // Rethrow longjmp if enabled -#if EXCEPTION_STACK_TRACES - || e instanceof EmscriptenSjLj // EXCEPTION_STACK_TRACES=1 will throw an instance of EmscriptenSjLj -#else - || e === Infinity // EXCEPTION_STACK_TRACES=0 will throw Infinity -#endif // EXCEPTION_STACK_TRACES + || e instanceof EmscriptenSjLj #endif || e === 'unwind' ) { diff --git a/src/runtime_exceptions.js b/src/runtime_exceptions.js index 1446b27dd2002..0b6aa5683354f 100644 --- a/src/runtime_exceptions.js +++ b/src/runtime_exceptions.js @@ -4,23 +4,35 @@ * SPDX-License-Identifier: MIT */ -#if EXCEPTION_STACK_TRACES && !WASM_EXCEPTIONS +#if !WASM_EXCEPTIONS + // Base Emscripten EH error class +#if EXCEPTION_STACK_TRACES class EmscriptenEH extends Error {} +#else +class EmscriptenEH {} +#endif #if SUPPORT_LONGJMP == 'emscripten' class EmscriptenSjLj extends EmscriptenEH {} #endif +#if !DISABLE_EXCEPTION_CATCHING class CppException extends EmscriptenEH { constructor(excPtr) { +#if EXCEPTION_STACK_TRACES super(excPtr); +#else + super(); +#endif this.excPtr = excPtr; -#if !DISABLE_EXCEPTION_CATCHING +#if !DISABLE_EXCEPTION_CATCHING && EXCEPTION_STACK_TRACES const excInfo = getExceptionMessage(excPtr); this.name = excInfo[0]; this.message = excInfo[1]; #endif } } -#endif // EXCEPTION_STACK_TRACES && !WASM_EXCEPTIONS +#endif + +#endif // !WASM_EXCEPTIONS diff --git a/test/codesize/test_codesize_cxx_ctors1.json b/test/codesize/test_codesize_cxx_ctors1.json index c5af8dc17687c..ada89930a22df 100644 --- a/test/codesize/test_codesize_cxx_ctors1.json +++ b/test/codesize/test_codesize_cxx_ctors1.json @@ -1,10 +1,10 @@ { - "a.out.js": 19214, - "a.out.js.gz": 7981, + "a.out.js": 19193, + "a.out.js.gz": 7970, "a.out.nodebug.wasm": 132638, "a.out.nodebug.wasm.gz": 49927, - "total": 151852, - "total_gz": 57908, + "total": 151831, + "total_gz": 57897, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_ctors2.json b/test/codesize/test_codesize_cxx_ctors2.json index 92f730bae4918..4bacbacb3ddd8 100644 --- a/test/codesize/test_codesize_cxx_ctors2.json +++ b/test/codesize/test_codesize_cxx_ctors2.json @@ -1,10 +1,10 @@ { - "a.out.js": 19191, - "a.out.js.gz": 7966, + "a.out.js": 19170, + "a.out.js.gz": 7957, "a.out.nodebug.wasm": 132064, "a.out.nodebug.wasm.gz": 49586, - "total": 151255, - "total_gz": 57552, + "total": 151234, + "total_gz": 57543, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_except.json b/test/codesize/test_codesize_cxx_except.json index 0ef6129a7520a..f5beaf9c15fb0 100644 --- a/test/codesize/test_codesize_cxx_except.json +++ b/test/codesize/test_codesize_cxx_except.json @@ -1,10 +1,10 @@ { - "a.out.js": 22875, - "a.out.js.gz": 8956, + "a.out.js": 23195, + "a.out.js.gz": 8968, "a.out.nodebug.wasm": 172516, "a.out.nodebug.wasm.gz": 57438, - "total": 195391, - "total_gz": 66394, + "total": 195711, + "total_gz": 66406, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_mangle.json b/test/codesize/test_codesize_cxx_mangle.json index 469c39c54514f..626d3a0d494a4 100644 --- a/test/codesize/test_codesize_cxx_mangle.json +++ b/test/codesize/test_codesize_cxx_mangle.json @@ -1,10 +1,10 @@ { - "a.out.js": 22925, - "a.out.js.gz": 8978, + "a.out.js": 23245, + "a.out.js.gz": 8990, "a.out.nodebug.wasm": 238957, "a.out.nodebug.wasm.gz": 79847, - "total": 261882, - "total_gz": 88825, + "total": 262202, + "total_gz": 88837, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_noexcept.json b/test/codesize/test_codesize_cxx_noexcept.json index f4d5c0e3e1bf0..5f42ba6c192f1 100644 --- a/test/codesize/test_codesize_cxx_noexcept.json +++ b/test/codesize/test_codesize_cxx_noexcept.json @@ -1,10 +1,10 @@ { - "a.out.js": 19214, - "a.out.js.gz": 7981, + "a.out.js": 19193, + "a.out.js.gz": 7970, "a.out.nodebug.wasm": 134661, "a.out.nodebug.wasm.gz": 50777, - "total": 153875, - "total_gz": 58758, + "total": 153854, + "total_gz": 58747, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_wasmfs.json b/test/codesize/test_codesize_cxx_wasmfs.json index 28af35c267494..e2c697449f464 100644 --- a/test/codesize/test_codesize_cxx_wasmfs.json +++ b/test/codesize/test_codesize_cxx_wasmfs.json @@ -1,10 +1,10 @@ { - "a.out.js": 7053, - "a.out.js.gz": 3325, + "a.out.js": 7033, + "a.out.js.gz": 3310, "a.out.nodebug.wasm": 172736, "a.out.nodebug.wasm.gz": 63329, - "total": 179789, - "total_gz": 66654, + "total": 179769, + "total_gz": 66639, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index 932ca953050e6..bec5c6ff55130 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -307,6 +307,11 @@ var EXITSTATUS; // include: runtime_stack_check.js // end include: runtime_stack_check.js // include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + // end include: runtime_exceptions.js // include: runtime_debug.js // end include: runtime_debug.js diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index f079efe8ca4e5..94e93d273ce87 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26256, - "a.out.js.gz": 11230, + "a.out.js": 26183, + "a.out.js.gz": 11173, "a.out.nodebug.wasm": 17668, "a.out.nodebug.wasm.gz": 8921, - "total": 43924, - "total_gz": 20151, + "total": 43851, + "total_gz": 20094, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index ec660a1c0d877..2a4df77df86ab 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 244367, + "a.out.js": 244411, "a.out.nodebug.wasm": 577484, - "total": 821851, + "total": 821895, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index 54ae98b7e078b..f4a791a3dee4c 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -327,6 +327,11 @@ function checkStackCookie() { } // end include: runtime_stack_check.js // include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + // end include: runtime_exceptions.js // include: runtime_debug.js var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index ecc52999be8a2..e38569d8c4b6b 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 56949, - "hello_world.js.gz": 17708, + "hello_world.js": 57052, + "hello_world.js.gz": 17748, "hello_world.wasm": 14850, "hello_world.wasm.gz": 7311, - "no_asserts.js": 26520, - "no_asserts.js.gz": 8849, + "no_asserts.js": 26623, + "no_asserts.js.gz": 8888, "no_asserts.wasm": 12010, "no_asserts.wasm.gz": 5880, - "strict.js": 54767, - "strict.js.gz": 17016, + "strict.js": 54870, + "strict.js.gz": 17055, "strict.wasm": 14850, "strict.wasm.gz": 7311, - "total": 179946, - "total_gz": 64075 + "total": 180255, + "total_gz": 64193 } diff --git a/tools/js_manipulation.py b/tools/js_manipulation.py index a052ac19be786..37e3c12f5b728 100644 --- a/tools/js_manipulation.py +++ b/tools/js_manipulation.py @@ -135,15 +135,7 @@ def make_invoke(sig): exceptional_ret = '\n return 0n;' if legal_sig[0] == 'j' else '' body = '%s%s;' % (ret, make_dynCall(sig, args)) # Create a try-catch guard that rethrows the Emscripten EH exception. - if settings.EXCEPTION_STACK_TRACES: - # Exceptions thrown from C++ and longjmps will be an instance of - # EmscriptenEH. - maybe_rethrow = 'if (!(e instanceof EmscriptenEH)) throw e;' - else: - # Exceptions thrown from C++ will be a pointer (number) and longjmp will - # throw the number Infinity. Use the compact and fast "e !== e+0" test to - # check if e was not a Number. - maybe_rethrow = 'if (e !== e+0) throw e;' + maybe_rethrow = 'if (!(e instanceof EmscriptenEH)) throw e;' ret = '''\ function invoke_%s(%s) {