Skip cleanups on unsupported targets

This commit is an update to the `AbortUnwindingCalls` MIR pass in the
compiler. Specifically a new boolean is added for "can this target
possibly unwind" and if that's `false` then terminators are all adjusted
to be unreachable/not present. The end result is that this fixes 140293
for wasm targets.

The motivation for this PR is that currently on WebAssembly targets the
usage of the `C-unwind` ABI can lead LLVM to either (a) emit
exception-handling instructions or (b) hit a LLVM-ICE-style codegen
error. WebAssembly as a base instruction set does not support unwinding
at all, and a later proposal to WebAssembly, the exception-handling
proposal, was what enabled this. This means that the current intent of
WebAssembly targets is that they maintain the baseline of "don't emit
exception-handling instructions unless enabled". The commit here is
intended to restore this behavior by skipping these instructions even
when `C-unwind` is present.

Exception-handling is a relatively tricky and also murky topic in
WebAssembly, however. There are two sets of instructions LLVM can emit
for WebAssembly exceptions, Rust's Emscripten target supports
exceptions, WASI targets do not, the LLVM flags to enable this are not
always obvious, and additionally this all touches on "changing
exception-handling behavior should be a target-level concern, not a
feature". Effectively WebAssembly's exception-handling integration into
Rust is not finalized at this time. The best idea at this time is that a
parallel set of targets will eventually be added which support
exceptions, but it's not clear if/when to do this. In the meantime the
goal is to keep existing targets working while still enabling
experimentation with exception-handling with `-Zbuild-std` and various
permutations of LLVM flags.

To that extent this commit does not blanket disable these landing pads
and cleanup routines for WebAssembly but instead checks to see if
panic=unwind is enabled or if `+exception-handling` is enabled. Tests
are updated here as well to account for this where, by default, using a
`C-unwind` ABI won't affect Rust codegen at all. If
`+exception-handling` is enabled, however, then Rust codegen will look
like native platforms where exceptions are caught and the program aborts.
More-or-less I've done my best to keep exceptions working on wasm where
it's possible to have them work, but turned them off where they're not
supposed to be emitted.
This commit is contained in:
Alex Crichton
2025-09-11 15:48:44 -07:00
parent 76c5ed2847
commit 88d7d20122
5 changed files with 86 additions and 27 deletions

View File

@@ -3,6 +3,7 @@ use rustc_ast::InlineAsmOptions;
use rustc_middle::mir::*;
use rustc_middle::span_bug;
use rustc_middle::ty::{self, TyCtxt, layout};
use rustc_span::sym;
use rustc_target::spec::PanicStrategy;
/// A pass that runs which is targeted at ensuring that codegen guarantees about
@@ -33,6 +34,19 @@ impl<'tcx> crate::MirPass<'tcx> for AbortUnwindingCalls {
return;
}
// Represent whether this compilation target fundamentally doesn't
// support unwinding at all at an ABI level. If this the target has no
// support for unwinding then cleanup actions, for example, are all
// unnecessary and can be considered unreachable.
//
// Currently this is only true for wasm targets on panic=abort when the
// `exception-handling` target feature is disabled. In such a
// configuration it's illegal to emit exception-related instructions so
// it's not possible to unwind.
let target_supports_unwinding = !(tcx.sess.target.is_like_wasm
&& tcx.sess.panic_strategy() == PanicStrategy::Abort
&& !tcx.asm_target_features(def_id).contains(&sym::exception_handling));
// Here we test for this function itself whether its ABI allows
// unwinding or not.
let body_ty = tcx.type_of(def_id).skip_binder();
@@ -54,12 +68,18 @@ impl<'tcx> crate::MirPass<'tcx> for AbortUnwindingCalls {
let Some(terminator) = &mut block.terminator else { continue };
let span = terminator.source_info.span;
// If we see an `UnwindResume` terminator inside a function that cannot unwind, we need
// to replace it with `UnwindTerminate`.
if let TerminatorKind::UnwindResume = &terminator.kind
&& !body_can_unwind
{
terminator.kind = TerminatorKind::UnwindTerminate(UnwindTerminateReason::Abi);
// If we see an `UnwindResume` terminator inside a function then:
//
// * If the target doesn't support unwinding at all, then this is an
// unreachable block.
// * If the body cannot unwind, we need to replace it with
// `UnwindTerminate`.
if let TerminatorKind::UnwindResume = &terminator.kind {
if !target_supports_unwinding {
terminator.kind = TerminatorKind::Unreachable;
} else if !body_can_unwind {
terminator.kind = TerminatorKind::UnwindTerminate(UnwindTerminateReason::Abi);
}
}
if block.is_cleanup {
@@ -93,8 +113,9 @@ impl<'tcx> crate::MirPass<'tcx> for AbortUnwindingCalls {
_ => continue,
};
if !call_can_unwind {
// If this function call can't unwind, then there's no need for it
if !call_can_unwind || !target_supports_unwinding {
// If this function call can't unwind, or if the target doesn't
// support unwinding at all, then there's no need for it
// to have a landing pad. This means that we can remove any cleanup
// registered for it (and turn it into `UnwindAction::Unreachable`).
let cleanup = block.terminator_mut().unwind_mut().unwrap();

View File

@@ -938,6 +938,7 @@ symbols! {
ermsb_target_feature,
exact_div,
except,
exception_handling: "exception-handling",
exchange_malloc,
exclusive_range_pattern,
exhaustive_integer_patterns,

View File

@@ -1,4 +1,9 @@
//@ compile-flags: -C panic=abort
//@ revisions: NONWASM WASM WASMEXN
//@ [NONWASM] ignore-wasm32
//@ [WASM] only-wasm32
//@ [WASMEXN] only-wasm32
//@ [WASMEXN] compile-flags: -Ctarget-feature=+exception-handling
// Test that `nounwind` attributes are also applied to extern `C-unwind` Rust functions
// when the code is compiled with `panic=abort`.
@@ -9,7 +14,9 @@
#[no_mangle]
pub unsafe extern "C-unwind" fn rust_item_that_can_unwind() {
// Handle both legacy and v0 symbol mangling.
// CHECK: call void @{{.*core9panicking19panic_cannot_unwind}}
// NONWASM: call void @{{.*core9panicking19panic_cannot_unwind}}
// WASMEXN: call void @{{.*core9panicking19panic_cannot_unwind}}
// WASM-NOT: call void @{{.*core9panicking19panic_cannot_unwind}}
may_unwind();
}

View File

@@ -1,4 +1,9 @@
//@ compile-flags: -C panic=abort
//@ revisions: NONWASM WASM WASMEXN
//@ [NONWASM] ignore-wasm32
//@ [WASM] only-wasm32
//@ [WASMEXN] only-wasm32
//@ [WASMEXN] compile-flags: -Ctarget-feature=+exception-handling
#![crate_type = "lib"]
@@ -9,7 +14,9 @@ extern "C-unwind" {
// CHECK: Function Attrs:{{.*}}nounwind
// CHECK-NEXT: define{{.*}}void @foo
// Handle both legacy and v0 symbol mangling.
// CHECK: call void @{{.*core9panicking19panic_cannot_unwind}}
// NONWASM: call void @{{.*core9panicking19panic_cannot_unwind}}
// WASMEXN: call void @{{.*core9panicking19panic_cannot_unwind}}
// WASM-NOT: call void @{{.*core9panicking19panic_cannot_unwind}}
#[no_mangle]
pub unsafe extern "C" fn foo() {
bar();

View File

@@ -1,8 +1,9 @@
//@ only-wasm32
//@ compile-flags: -C panic=unwind -Z emscripten-wasm-eh
//@ revisions: WASM WASMEXN
//@ [WASMEXN] compile-flags: -C panic=unwind -Z emscripten-wasm-eh
#![crate_type = "lib"]
#![feature(core_intrinsics, wasm_exception_handling_intrinsics)]
#![feature(core_intrinsics, wasm_exception_handling_intrinsics, link_llvm_intrinsics)]
extern "C-unwind" {
fn may_panic();
@@ -22,7 +23,8 @@ impl Drop for LogOnDrop {
}
}
// CHECK-LABEL: @test_cleanup() {{.*}} @__gxx_wasm_personality_v0
// WASM-LABEL: @test_cleanup() {{.*}}
// WASMEXN-LABEL: @test_cleanup() {{.*}} @__gxx_wasm_personality_v0
#[no_mangle]
pub fn test_cleanup() {
let _log_on_drop = LogOnDrop;
@@ -30,12 +32,16 @@ pub fn test_cleanup() {
may_panic();
}
// CHECK-NOT: call
// CHECK: invoke void @may_panic()
// CHECK: %cleanuppad = cleanuppad within none []
// WASMEXN-NOT: call
// WASMEXN: invoke void @may_panic()
// WASMEXN: %cleanuppad = cleanuppad within none []
//
// WASM: call void @may_panic()
// WASM-NOT: invoke void @may_panic()
}
// CHECK-LABEL: @test_rtry() {{.*}} @__gxx_wasm_personality_v0
// WASM-LABEL: @test_rtry() {{.*}}
// WASMEXN-LABEL: @test_rtry() {{.*}} @__gxx_wasm_personality_v0
#[no_mangle]
pub fn test_rtry() {
unsafe {
@@ -51,23 +57,40 @@ pub fn test_rtry() {
);
}
// CHECK-NOT: call
// CHECK: invoke void @may_panic()
// CHECK: {{.*}} = catchswitch within none [label {{.*}}] unwind to caller
// CHECK: {{.*}} = catchpad within {{.*}} [ptr null]
// CHECK: catchret
// WASMEXN-NOT: call
// WASMEXN: invoke void @may_panic()
// WASMEXN: {{.*}} = catchswitch within none [label {{.*}}] unwind to caller
// WASMEXN: {{.*}} = catchpad within {{.*}} [ptr null]
// WASMEXN: catchret
// WASM: call void @may_panic()
// WASM-NOT: invoke void @may_panic()
// WASM-NOT: catchswitch
// WASM-NOT: catchpad
// WASM-NOT: catchret
}
// Make sure the intrinsic is not inferred as nounwind. This is a regression test for #132416.
// CHECK-LABEL: @test_intrinsic() {{.*}} @__gxx_wasm_personality_v0
//
// Note that this test uses the raw `wasm_throw` intrinsic because the one from
// libstd was built with `-Cpanic=abort` and it's technically not valid to use
// when this crate is compiled with `-Cpanic=unwind`.
//
// WASMEXN-LABEL: @test_intrinsic() {{.*}} @__gxx_wasm_personality_v0
#[no_mangle]
#[cfg(wasmexn)]
pub fn test_intrinsic() {
let _log_on_drop = LogOnDrop;
unsafe extern "C-unwind" {
#[link_name = "llvm.wasm.throw"]
fn wasm_throw(tag: i32, ptr: *mut u8) -> !;
}
unsafe {
core::arch::wasm32::throw::<0>(core::ptr::null_mut());
wasm_throw(0, core::ptr::null_mut());
}
// CHECK-NOT: call
// CHECK: invoke void @llvm.wasm.throw(i32 noundef 0, ptr noundef null)
// CHECK: %cleanuppad = cleanuppad within none []
// WASMEXN-NOT: call
// WASMEXN: invoke void @llvm.wasm.throw(i32 noundef 0, ptr noundef null)
// WASMEXN: %cleanuppad = cleanuppad within none []
}