Skip to content

Link the Wasm EH runtime under -fwasm-exceptions without requiring C++ - #27496

Open
guybedford wants to merge 1 commit into
emscripten-core:mainfrom
guybedford:wasm-eh-libunwind
Open

Link the Wasm EH runtime under -fwasm-exceptions without requiring C++#27496
guybedford wants to merge 1 commit into
emscripten-core:mainfrom
guybedford:wasm-eh-libunwind

Conversation

@guybedford

@guybedford guybedford commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

This makes -fwasm-exceptions link the Wasm EH runtime independently of C++ linking.

Wasm EH objects can be produced by non-C++ frontends - notably rustc, whose precompiled std uses Wasm exceptions for unwinding and which invokes emcc (not em++) as its linker. Since DEFAULT_TO_CXX was disabled by default in #27469, any rust-driven emcc link now fails with:

wasm-ld: error: libstd-*.rlib: undefined symbol: __cpp_exception
wasm-ld: error: libpanic_unwind-*.rlib: undefined symbol: _Unwind_RaiseException

because both libunwind and the __cpp_exception tag (previously defined in libc++abi) were only linked under LINK_AS_CXX. The tag and the unwinding runtime are language-agnostic runtime support for the Wasm EH LLVM lowering, not part of the C++ ABI surface.

This PR now includes #27498 as its base, which provides the same _Unwind_* linkability for the non-Wasm-EH modes on the JS side.

What's implemented:

Test coverage extends test_libunwind from #27498 with wasm_eh and wasm_legacy_eh variants (completing its TODO), and adds test_cpp_exception_tag linking an asm object that throws the tag directly from a C caller - the same reference pattern as rustc-generated catch/throw sites - which fails to link without this change. The now-unneeded DEFAULT_TO_CXX workaround is dropped from test_wasi_with_sjlj. Existing C++ Wasm EH coverage (core0.test_exceptions_*) passes unchanged.

Made with AI assistance under my review

@hoodmane

hoodmane commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Is this really needed? Rust with wasm eh has been working, so if it's needed it should be fixing a regression.

@hoodmane

hoodmane commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Since DEFAULT_TO_CXX was disabled by default

Oh I see. This is pretty confusing because what does it mean for the linker?

@sbc100
sbc100 requested a review from aheejin August 7, 2026 06:02

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How does this work on other platforms? Does rust use exception thrown/catching without linking libc++abi there? Does rust use clang (not clang++) during linking?

@guybedford

Copy link
Copy Markdown
Collaborator Author

How does this work on other platforms? Does rust use exception thrown/catching without linking libc++abi there? Does rust use clang (not clang++) during linking?

Yes and yes. rustc hardcodes emcc for linking, and Rust panics only use the language-agnostic _Unwind_* layer (libgcc_s/libunwind), which the C driver links unconditionally on native targets. libc++abi is only the C++ personality/_cxa* layer above that, which Rust replaces with its own personality. This PR gives Wasm the same split: the __cpp_exception tag + Unwind-wasm.c are the base layer (LLVM's wasm EH lowering throws that tag for all languages, despite the name), libc++abi stays C++-only.

Comment made with AI assistance

@sbc100

sbc100 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

I see. I hadn't realized that libunwind was not a C++ specific thing.

I was able to reproduce this using my desktop clang:

$ clang -Wl,--trace-symbol=_Unwind_RaiseException -Wl,-u_Unwind_RaiseException -fuse-ld=lld --unwindlib=libunwind -rtlib=compiler-rt  ~/test/hello.c -v
Debian clang version 21.1.8 (3)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/lib/llvm-21/bin
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/13
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/14
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/15
Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/15
Candidate multilib: .;@m64
Candidate multilib: 32;@m32
Candidate multilib: x32;@mx32
Selected multilib: .;@m64
 "/usr/lib/llvm-21/bin/clang" -cc1 -triple x86_64-pc-linux-gnu -emit-obj -dumpdir a- -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name hello.c -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=all -fmath-errno -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/usr/local/google/home/sbc/dev/wasm/emscripten -v -fcoverage-compilation-dir=/usr/local/google/home/sbc/dev/wasm/emscripten -resource-dir /usr/lib/llvm-21/lib/clang/21 -internal-isystem /usr/lib/llvm-21/lib/clang/21/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -ferror-limit 19 -fmessage-length=124 -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcolor-diagnostics -faddrsig -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /tmp/hello-4585ba.o -x c /usr/local/google/home/sbc/test/hello.c
clang -cc1 version 21.1.8 based upon LLVM 21.1.8 default target x86_64-pc-linux-gnu
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/15/../../../../x86_64-linux-gnu/include"
ignoring nonexistent directory "/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/llvm-21/lib/clang/21/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
 "/usr/lib/llvm-21/bin/ld.lld" --hash-style=gnu --build-id --eh-frame-hdr -m elf_x86_64 -pie -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o a.out /lib/x86_64-linux-gnu/Scrt1.o /lib/x86_64-linux-gnu/crti.o /usr/lib/llvm-21/lib/clang/21/lib/linux/clang_rt.crtbegin-x86_64.o -L/usr/lib/gcc/x86_64-linux-gnu/15 -L/usr/lib/gcc/x86_64-linux-gnu/15/../../../../lib64 -L/lib/x86_64-linux-gnu -L/lib/../lib64 -L/usr/lib/x86_64-linux-gnu -L/usr/lib64 -L/lib -L/usr/lib --trace-symbol=_Unwind_RaiseException -u_Unwind_RaiseException /tmp/hello-4585ba.o /usr/lib/llvm-21/lib/clang/21/lib/linux/libclang_rt.builtins-x86_64.a --as-needed -lunwind --no-as-needed -lc /usr/lib/llvm-21/lib/clang/21/lib/linux/libclang_rt.builtins-x86_64.a --as-needed -lunwind --no-as-needed /usr/lib/llvm-21/lib/clang/21/lib/linux/clang_rt.crtend-x86_64.o /lib/x86_64-linux-gnu/crtn.o
<internal>: reference to _Unwind_RaiseException
/lib/x86_64-linux-gnu/libunwind.so: shared definition of _Unwind_RaiseException
<internal>: reference to _Unwind_RaiseException

Comment thread tools/system_libs.py Outdated
Comment thread test/test_other.py Outdated
Comment thread test/test_other.py Outdated
Comment thread test/test_other.py Outdated
Comment thread test/test_other.py
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 7, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 7, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 7, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 7, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 7, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 7, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 7, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
Comment thread tools/system_libs.py Outdated
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 8, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
Comment thread test/test_other.py
@guybedford
guybedford force-pushed the wasm-eh-libunwind branch 2 times, most recently from 382bb6d to 43189b4 Compare August 8, 2026 00:26
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 8, 2026
Extract `_Unwind_*` functions and `uncaughtExceptionCount` /
`exceptionLast` JS state from libcore.js and libexceptions.js into a
dedicated `libunwind.js`.

This new libunwind.js is included as long as wasm EH is not being
used.

Also, invert the dependency between `__cxa_throw` and
`_Unwind_RaiseException` so that `__cxa_throw` now calls
`_Unwind_RaiseException` rather than the other way around.  This is
important as it allows C programs (or rather non-C++ programs like Rust) to
call `_Unwind_RaiseException` without linking as C++.

Inspired by emscripten-core#27496
sbc100 added a commit that referenced this pull request Aug 8, 2026
…eption` (#27498)

It turns out the libunwind symbols are supposed to always be available,
enen non-C++ programs. This means that `__cxa_throw` should be
implemented in terms of `_Unwind_RaiseException` and not the other way
around.

- Moves the libunwind code into a new libunwind.js file (only use for
Emscripten EH).
- Add support for `_Unwind_Resume`
- Implement `__cxa_throw` and other exception-handling APIs in terms of
the lower level `_Unwind` API.

Inspired by #27496
Comment thread tools/system_libs.py
# unwinding runtime and the `__cpp_exception` tag it defines are linked
# independently of C++. When WASM_EXCEPTIONS is not enabled, `_Unwind_*`
# symbols are instead provided by JS stubs in libcore.js (under
# LINK_AS_CXX).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this full comment is no longer needed. libunwind should always be linked into all programs one or another. I should have known that before.

How about just:

`libunwind` is implement in JS, and not nativly, when Emscripten EH is used.

Comment thread ChangeLog.md
- `-fwasm-exceptions` now links the Wasm EH runtime (libunwind and the
`__cpp_exception` tag) independently of C++ linking, so Wasm EH objects from
non-C++ frontends (e.g. rustc, which links via `emcc`) link without
requiring `em++` or `-sDEFAULT_TO_CXX`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Add PR number here.

Comment thread tools/system_libs.py
Comment on lines +2489 to +2491
# independently of C++. When WASM_EXCEPTIONS is not enabled, `_Unwind_*`
# symbols are instead provided by JS stubs in libcore.js (under
# LINK_AS_CXX).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this mean we need LINK_AS_CXX in case of Emscripten EH? This sounds weird. If LINK_AS_CXX should not be required for linking linbunwind in Wasm EH, shouldn't that be true also for Emscripten EH too?

Comment thread test/test_other.py
Comment on lines 8837 to +8839
'exceptions': (['-fexceptions'],),
'wasm_eh': (['-fwasm-exceptions'],),
'wasm_legacy_eh': (['-fwasm-exceptions', '-sWASM_LEGACY_EXCEPTIONS'],),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we use @with_all_eh_sjlj instead of this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IIUC that does not include the empty case (i.e. no exception handling)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants