From 372ba0d2222bcc7fb5e8f032afa36ad896c9d94f Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 12 Feb 2026 14:45:31 -0600 Subject: [PATCH] Add experimental support for C++ exceptions (#590) > **Note**: this PR is me weaving together the work of many others. For example @yerzham's and @cpetig's work on #565 pretty much shaped this PR. Thank you! This commit adds initial configuration support for building wasi-sdk with support for C++ exceptions in WebAssembly. A small test is included which is exercised in CI at this time, but otherwise this does not update CI to actually ship sdk builds with C++ exceptions enabled. Instead the intention here is to make it easier to test builds with support for C++ exceptions and centralize planning/development around this. The goal here is to get things compiling to the point that applications can be compiled. I haven't thoroughly tested C++ exceptions and as evident in this PR it still requires changes in LLVM. Some small logic is added here to apply a `*.patch` file to LLVM to avoid needing a new submodule fork or waiting for upstream changes. The intention is that this `*.patch` is short-lived once the changes are officially merged into LLVM itself. The `*.patch` included here contains llvm/llvm-project#168449 as well as another minor edit I found was necessary to get things compiling locally. Given the discussion on that PR it looks like once LLVM is updated with that merged the extra part of the `*.patch` won't be necessary. This PR notably does not enable shared libraries when exceptions are enabled. I don't know enough about how things are supposed to work to be able to fully diagnose the compilation errors I'm seeing if shared libraries are enabled. This is something I'd hope would be fixed before actually shipping exceptions support. This PR then additionally folds in [this gist][gist] for various bits of build logic to this repository itself. Finally, this PR includes some documentation about the current status of exceptions and links to various tracking issues too. [gist]: https://gist.github.com/yerzham/302efcec6a2e82c1e8de4aed576ea29d --- .github/actions/install-deps/action.yml | 2 +- .github/workflows/main.yml | 11 ++++- CppExceptions.md | 60 +++++++++++++++++++++++++ README.md | 4 ++ cmake/wasi-sdk-sysroot.cmake | 29 ++++++++++-- src/llvm-pr-168449.patch | 28 ++++++++++++ tests/CMakeLists.txt | 18 +++++++- tests/general/exceptions.cc | 17 +++++++ tests/testcase.sh | 2 +- 9 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 CppExceptions.md create mode 100644 src/llvm-pr-168449.patch create mode 100644 tests/general/exceptions.cc diff --git a/.github/actions/install-deps/action.yml b/.github/actions/install-deps/action.yml index 2e4967a..e3d00f8 100644 --- a/.github/actions/install-deps/action.yml +++ b/.github/actions/install-deps/action.yml @@ -7,7 +7,7 @@ runs: - name: Setup `wasmtime` for tests uses: bytecodealliance/actions/wasmtime/setup@v1 with: - version: "29.0.1" + version: "41.0.3" - name: Install ccache, ninja (macOS) run: brew install ccache ninja if: runner.os == 'macOS' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e2df4b..8811575 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -155,8 +155,14 @@ jobs: key: 0-cache-${{ matrix.artifact }}-${{ github.run_id }} build-only-sysroot: - name: Build only sysroot + name: Build only sysroot - ${{ matrix.name }} runs-on: ubuntu-24.04 + strategy: + matrix: + include: + - name: default + - name: exceptions + defines: -DWASI_SDK_EXCEPTIONS=ON steps: - uses: actions/checkout@v4 with: @@ -172,7 +178,8 @@ jobs: -DWASI_SDK_INCLUDE_TESTS=ON \ -DWASI_SDK_CPU_CFLAGS="" \ -DCMAKE_C_LINKER_DEPFILE_SUPPORTED=OFF \ - -DCMAKE_CXX_LINKER_DEPFILE_SUPPORTED=OFF + -DCMAKE_CXX_LINKER_DEPFILE_SUPPORTED=OFF \ + ${{ matrix.defines }} - run: ninja -C build - run: ctest --output-on-failure --parallel 10 --test-dir build/tests diff --git a/CppExceptions.md b/CppExceptions.md new file mode 100644 index 0000000..7ea1e2d --- /dev/null +++ b/CppExceptions.md @@ -0,0 +1,60 @@ +# Support for C++ Exceptions + +The released artifacts for wasi-sdk at this time do not support C++ exceptions. +LLVM and Clang, however, have support for C++ exceptions in WebAssembly and this +is intended to serve as documentation of the current state of affairs of using +C++ exceptions. It should be noted though that the current status of C++ +exceptions support is not intended to be the final state of support, and this is +all continuing to be iterated on over time. + +## Building wasi-sdk with exceptions + +When building the sysroot with wasi-sdk you can pass `-DWASI_SDK_EXCEPTIONS=ON` +to enable support for C++ exceptions. For example: + +```shell script +$ cmake -G Ninja -B build/sysroot -S . \ + -DCMAKE_TOOLCHAIN_FILE=$path/to/wasi-sdk-p1.cmake \ + -DWASI_SDK_EXCEPTIONS=ON +``` + +The C++ standard library will be compiled with support for exceptions for the +desired targets and the resulting sysroot supports using exceptions. + +## Compiling code with C++ exceptions + +Currently extra compilation flags are required to fully support C++ exceptions. +Without these flags programs using C++ exceptions will not work correctly: + +* `-fwasm-exceptions` - needed to enable the WebAssembly exception-handling + proposal. +* `-mllvm -wasm-use-legacy-eh=false` - indicates that the standard WebAssembly + exception-handling instructions should be used. +* `-lunwind` - links in support for unwinding which C++ exceptions requires. + +This can be specified for example with: + +```shell script +$ export CFLAGS="-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false" +$ export LDFLAGS="-lunwind" +``` + +## Limitations + +Currently C++ exceptions support in wasi-sdk does not support shared libraries. +Fixing this will require resolving some miscellaneous build issues in this +repository itself. + +## Future Plans + +There are a few tracking issues with historical discussion about C++ exceptions +support in wasi-sdk such as [#334](https://github.com/WebAssembly/wasi-sdk/issues/334) +and [#565](https://github.com/WebAssembly/wasi-sdk/issues/565). The major +remaining items are: + +* Figure out support for shared libraries. +* Determine how to ship a sysroot that supports both with-and-without + exceptions. +* Figure out how to avoid the need for extra compiler flags when using + exceptions. +* Figure out if a new wasm target is warranted. diff --git a/README.md b/README.md index 014016d..97bb4f5 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ in compiling WebAssembly code. Supported CMake flags are: * `-DWASI_SDK_INCLUDE_TESTS=ON` - used for building tests. * `-DWASI_SDK_CPU_CFLAGS=..` - used to specify CFLAGS to tweak wasm features to enable/disable. The default is `-mcpu=lime1`. +* `-DWASI_SDK_LTO=ON` - whether to enable/disable builds of LTO-capable + libraries as part of the build. +* `-DWASI_SDK_EXCEPTIONS=ON` - whether to enable/disable support for C++ + exceptions, see [CppExceptions.md](./CppExceptions.md) for more information. * `-DWASI_SDK_TEST_HOST_TOOLCHAIN=ON` - test the host toolchain's wasi-libc and sysroot libraries, don't build or use fresh libraries for tests. * `-DWASI_SDK_TARGETS=..` - a list of targets to build, by default all WASI diff --git a/cmake/wasi-sdk-sysroot.cmake b/cmake/wasi-sdk-sysroot.cmake index 72088d9..7be2a18 100644 --- a/cmake/wasi-sdk-sysroot.cmake +++ b/cmake/wasi-sdk-sysroot.cmake @@ -24,6 +24,7 @@ option(WASI_SDK_DEBUG_PREFIX_MAP "Pass `-fdebug-prefix-map` for built artifacts" option(WASI_SDK_INCLUDE_TESTS "Whether or not to build tests by default" OFF) option(WASI_SDK_INSTALL_TO_CLANG_RESOURCE_DIR "Whether or not to modify the compiler's resource directory" OFF) option(WASI_SDK_LTO "Whether or not to build LTO assets" ON) +option(WASI_SDK_EXCEPTIONS "Whether or not C++ exceptions are enabled" OFF) set(WASI_SDK_CPU_CFLAGS "-mcpu=lime1" CACHE STRING "CFLAGS to specify wasm features to enable") set(wasi_tmp_install ${CMAKE_CURRENT_BINARY_DIR}/install) @@ -89,6 +90,8 @@ function(define_compiler_rt target) -DCOMPILER_RT_BUILD_GWP_ASAN=OFF -DCMAKE_C_COMPILER_TARGET=${target} -DCMAKE_C_FLAGS=${WASI_SDK_CPU_CFLAGS} + -DCMAKE_CXX_FLAGS=${WASI_SDK_CPU_CFLAGS} + -DCMAKE_ASM_FLAGS=${WASI_SDK_CPU_CFLAGS} -DCMAKE_INSTALL_PREFIX=${wasi_resource_dir} EXCLUDE_FROM_ALL ON USES_TERMINAL_CONFIGURE ON @@ -231,6 +234,17 @@ function(define_libcxx_sub target target_suffix extra_target_flags extra_libdir_ --sysroot ${wasi_sysroot} -resource-dir ${wasi_resource_dir}) + if (WASI_SDK_EXCEPTIONS) + # TODO: lots of builds fail with shared libraries and `-fPIC`. Looks like + # things are maybe changing in llvm/llvm-project#159143 but otherwise I'm at + # least not really sure what the state of shared libraries and exceptions + # are. For now shared libraries are disabled and supporting them is left for + # a future endeavor. + set(pic OFF) + set(runtimes "libunwind;${runtimes}") + list(APPEND extra_flags -fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) + endif() + set(extra_cflags_list ${CMAKE_C_FLAGS} ${extra_flags}) list(JOIN extra_cflags_list " " extra_cflags) set(extra_cxxflags_list ${CMAKE_CXX_FLAGS} ${extra_flags}) @@ -254,14 +268,14 @@ function(define_libcxx_sub target target_suffix extra_target_flags extra_libdir_ -DLLVM_COMPILER_CHECKED=ON -DLIBCXX_ENABLE_SHARED:BOOL=${pic} -DLIBCXX_ENABLE_EXPERIMENTAL_LIBRARY:BOOL=OFF - -DLIBCXX_ENABLE_EXCEPTIONS:BOOL=OFF + -DLIBCXX_ENABLE_EXCEPTIONS:BOOL=${WASI_SDK_EXCEPTIONS} -DLIBCXX_ENABLE_FILESYSTEM:BOOL=ON -DLIBCXX_ENABLE_ABI_LINKER_SCRIPT:BOOL=OFF -DLIBCXX_CXX_ABI=libcxxabi -DLIBCXX_CXX_ABI_INCLUDE_PATHS=${llvm_proj_dir}/libcxxabi/include -DLIBCXX_HAS_MUSL_LIBC:BOOL=ON -DLIBCXX_ABI_VERSION=2 - -DLIBCXXABI_ENABLE_EXCEPTIONS:BOOL=OFF + -DLIBCXXABI_ENABLE_EXCEPTIONS:BOOL=${WASI_SDK_EXCEPTIONS} -DLIBCXXABI_ENABLE_SHARED:BOOL=${pic} -DLIBCXXABI_SILENT_TERMINATE:BOOL=ON -DLIBCXXABI_ENABLE_THREADS:BOOL=ON @@ -270,12 +284,18 @@ function(define_libcxx_sub target target_suffix extra_target_flags extra_libdir_ -DLIBCXXABI_BUILD_EXTERNAL_THREAD_LIBRARY:BOOL=OFF -DLIBCXXABI_HAS_WIN32_THREAD_API:BOOL=OFF -DLIBCXXABI_ENABLE_PIC:BOOL=${pic} - -DLIBCXXABI_USE_LLVM_UNWINDER:BOOL=OFF + -DLIBCXXABI_USE_LLVM_UNWINDER:BOOL=${WASI_SDK_EXCEPTIONS} + -DLIBUNWIND_ENABLE_SHARED:BOOL=${pic} + -DLIBUNWIND_ENABLE_THREADS:BOOL=ON + -DLIBUNWIND_USE_COMPILER_RT:BOOL=ON + -DLIBUNWIND_INCLUDE_TESTS:BOOL=OFF -DUNIX:BOOL=ON -DCMAKE_C_FLAGS=${extra_cflags} + -DCMAKE_ASM_FLAGS=${extra_cflags} -DCMAKE_CXX_FLAGS=${extra_cxxflags} -DLIBCXX_LIBDIR_SUFFIX=/${target}${extra_libdir_suffix} -DLIBCXXABI_LIBDIR_SUFFIX=/${target}${extra_libdir_suffix} + -DLIBUNWIND_LIBDIR_SUFFIX=/${target}${extra_libdir_suffix} -DLIBCXX_INCLUDE_TESTS=OFF -DLIBCXX_INCLUDE_BENCHMARKS=OFF @@ -290,6 +310,9 @@ function(define_libcxx_sub target target_suffix extra_target_flags extra_libdir_ USES_TERMINAL_CONFIGURE ON USES_TERMINAL_BUILD ON USES_TERMINAL_INSTALL ON + PATCH_COMMAND + ${CMAKE_COMMAND} -E chdir .. bash -c + "git apply ${CMAKE_SOURCE_DIR}/src/llvm-pr-168449.patch || git apply ${CMAKE_SOURCE_DIR}/src/llvm-pr-168449.patch -R --check" ) endfunction() diff --git a/src/llvm-pr-168449.patch b/src/llvm-pr-168449.patch new file mode 100644 index 0000000..a0da1d8 --- /dev/null +++ b/src/llvm-pr-168449.patch @@ -0,0 +1,28 @@ +diff --git a/libunwind/src/assembly.h b/libunwind/src/assembly.h +index f8e83e138eff..c5097d25b0c6 100644 +--- a/libunwind/src/assembly.h ++++ b/libunwind/src/assembly.h +@@ -249,6 +249,9 @@ aliasname: \ + #define WEAK_ALIAS(name, aliasname) + #define NO_EXEC_STACK_DIRECTIVE + ++#elif defined(__wasm__) ++#define NO_EXEC_STACK_DIRECTIVE ++ + // clang-format on + #else + +diff --git a/libunwind/src/config.h b/libunwind/src/config.h +index deb5a4d4d73d..23c9f012cbcf 100644 +--- a/libunwind/src/config.h ++++ b/libunwind/src/config.h +@@ -66,7 +66,8 @@ + #define _LIBUNWIND_EXPORT + #define _LIBUNWIND_HIDDEN + #else +- #if !defined(__ELF__) && !defined(__MACH__) && !defined(_AIX) ++ #if !defined(__ELF__) && !defined(__MACH__) && !defined(_AIX) && \ ++ !defined(__wasm__) + #define _LIBUNWIND_EXPORT __declspec(dllexport) + #define _LIBUNWIND_HIDDEN + #else diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index edbb129..a213392 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -67,7 +67,12 @@ function(add_testcase runwasi test) # Apply language-specific options and dependencies. if(test MATCHES "cc$") - target_compile_options(${target_name} PRIVATE -fno-exceptions) + if(WASI_SDK_EXCEPTIONS) + target_compile_options(${target_name} PRIVATE -fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) + target_link_options(${target_name} PRIVATE -lunwind) + else() + target_compile_options(${target_name} PRIVATE -fno-exceptions) + endif() if(NOT WASI_SDK_TEST_HOST_TOOLCHAIN) add_dependencies(${target_name} libcxx-${target}) endif() @@ -84,11 +89,20 @@ function(add_testcase runwasi test) endif() if(runwasi) + set(runner ${runwasi}) + if(${runner} MATCHES wasmtime) + if(target MATCHES threads) + set(runner "${runner} -Wshared-memory") + endif() + if(WASI_SDK_EXCEPTIONS) + set(runner "${runner} -Wexceptions") + endif() + endif() add_test( NAME test-${target_name} COMMAND bash ../testcase.sh - "${runwasi}" + ${runner} ${test} $ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/tests/general/exceptions.cc b/tests/general/exceptions.cc new file mode 100644 index 0000000..3daed98 --- /dev/null +++ b/tests/general/exceptions.cc @@ -0,0 +1,17 @@ +#include +#include + +int main() { +#ifdef __wasm_exception_handling__ + try { + throw std::runtime_error("An error occurred"); + abort(); + } catch (const std::runtime_error& e) { + // .. + return 0; + } + abort(); +#else + return 0; +#endif +} diff --git a/tests/testcase.sh b/tests/testcase.sh index 551eec3..d2c5a8c 100755 --- a/tests/testcase.sh +++ b/tests/testcase.sh @@ -46,7 +46,7 @@ fi # Run the test, capturing stdout, stderr, and the exit status. exit_status=0 -"$runwasi" $env $dir "$wasm" $dirarg \ +$runwasi $env $dir "$wasm" $dirarg \ < "$stdin" \ > "$stdout_observed" \ 2> "$stderr_observed" \