[clang] [CodeGen][Objective-C] Add WebAssembly `@finally` support (PR #218967)
Hendrik Hübner via cfe-commits
cfe-commits at lists.llvm.org
Wed Aug 26 11:51:49 PDT 2026
https://github.com/HendrikHuebner updated https://github.com/llvm/llvm-project/pull/218967
>From be662fa39fb2553ec3889098db0ff28563d02b99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hendrik=20H=C3=BCbner?= <hhuebner at MacBookPro.lan>
Date: Wed, 26 Aug 2026 19:44:16 +0200
Subject: [PATCH] [CodeGen][ObjC] Implement @finally for WebAssembly EH
---
clang/lib/CodeGen/CGException.cpp | 39 +++-
clang/lib/CodeGen/CGObjCRuntime.cpp | 13 +-
.../CodeGenObjC/gnustep2-wasm32-finally.m | 204 ++++++++++++++++++
clang/test/CodeGenObjCXX/wasm32-eh-objcxx.mm | 37 ++++
4 files changed, 279 insertions(+), 14 deletions(-)
create mode 100644 clang/test/CodeGenObjC/gnustep2-wasm32-finally.m
diff --git a/clang/lib/CodeGen/CGException.cpp b/clang/lib/CodeGen/CGException.cpp
index bc09fe767de45..fe6b050ac9100 100644
--- a/clang/lib/CodeGen/CGException.cpp
+++ b/clang/lib/CodeGen/CGException.cpp
@@ -1403,6 +1403,7 @@ namespace {
// If the end of the finally is reachable, check whether this was
// for EH. If so, rethrow.
+ const bool IsWasm = EHPersonality::get(CGF).isWasmPersonality();
if (CGF.HaveInsertPoint()) {
llvm::BasicBlock *RethrowBB = CGF.createBasicBlock("finally.rethrow");
llvm::BasicBlock *ContBB = CGF.createBasicBlock("finally.cont");
@@ -1412,15 +1413,15 @@ namespace {
CGF.Builder.CreateCondBr(ShouldRethrow, RethrowBB, ContBB);
CGF.EmitBlock(RethrowBB);
- if (SavedExnVar) {
- CGF.EmitRuntimeCallOrInvoke(RethrowFn, CGF.Builder.CreateAlignedLoad(
- CGF.Int8PtrTy, SavedExnVar,
- CGF.getPointerAlign()));
-
+ if (IsWasm || !SavedExnVar) {
+ CGF.EmitNoreturnRuntimeCallOrInvoke(RethrowFn, {});
} else {
- CGF.EmitRuntimeCallOrInvoke(RethrowFn);
+ CGF.EmitRuntimeCallOrInvoke(
+ RethrowFn,
+ CGF.Builder.CreateAlignedLoad(CGF.Int8PtrTy, SavedExnVar,
+ CGF.getPointerAlign()));
+ CGF.Builder.CreateUnreachable();
}
- CGF.Builder.CreateUnreachable();
CGF.EmitBlock(ContBB);
@@ -1506,15 +1507,34 @@ void CodeGenFunction::FinallyInfo::exit(CodeGenFunction &CGF) {
EHCatchScope &catchScope = cast<EHCatchScope>(*CGF.EHStack.begin());
llvm::BasicBlock *catchBB = catchScope.getHandler(0).Block;
+ llvm::BasicBlock *DispatchBlock = nullptr;
+ if (catchScope.hasEHBranches())
+ DispatchBlock = catchScope.getCachedEHDispatchBlock();
CGF.popCatchScope();
+ llvm::CatchPadInst *CPI = nullptr;
+
// If there are any references to the catch-all block, emit it.
if (catchBB->use_empty()) {
delete catchBB;
} else {
+ SaveAndRestore RestoreCurrentFuncletPad(CGF.CurrentFuncletPad);
+ if (EHPersonality::get(CGF).isWasmPersonality() && DispatchBlock) {
+ auto *CatchSwitch =
+ cast<llvm::CatchSwitchInst>(DispatchBlock->getFirstNonPHIIt());
+ llvm::BasicBlock *CatchStartBlock =
+ CatchSwitch->hasUnwindDest() ? CatchSwitch->getSuccessor(1)
+ : CatchSwitch->getSuccessor(0);
+ CPI = cast<llvm::CatchPadInst>(CatchStartBlock->getFirstNonPHIIt());
+ CGF.CurrentFuncletPad = CPI;
+ }
+
CGBuilderTy::InsertPoint savedIP = CGF.Builder.saveAndClearIP();
CGF.EmitBlock(catchBB);
+ if (CPI)
+ CGF.EHStack.pushCleanup<CatchRetScope>(NormalCleanup, CPI);
+
llvm::Value *exn = nullptr;
// If there's a begin-catch function, call it.
@@ -1535,6 +1555,11 @@ void CodeGenFunction::FinallyInfo::exit(CodeGenFunction &CGF) {
// Thread a jump through the finally cleanup.
CGF.EmitBranchThroughCleanup(RethrowDest);
+ // The catchret must be emitted while the catchpad is active. The branch
+ // through the finally cleanup is then resolved after leaving the catchpad.
+ if (CPI)
+ CGF.PopCleanupBlock();
+
CGF.Builder.restoreIP(savedIP);
}
diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp
index 099622f690678..e539e905a5d00 100644
--- a/clang/lib/CodeGen/CGObjCRuntime.cpp
+++ b/clang/lib/CodeGen/CGObjCRuntime.cpp
@@ -150,22 +150,18 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
if (S.getNumCatchStmts())
Cont = CGF.getJumpDestInCurrentScope("eh.cont");
- bool useFunclets = EHPersonality::get(CGF).usesFuncletPads();
bool IsWasm = EHPersonality::get(CGF).isWasmPersonality();
bool IsMSVC = EHPersonality::get(CGF).isMSVCPersonality();
CodeGenFunction::FinallyInfo FinallyInfo;
if (const ObjCAtFinallyStmt *Finally = S.getFinallyStmt()) {
- if (!useFunclets) {
+ if (!IsMSVC) {
// The finally statement is executed as a cleanup for the normal and
// exceptional control flow out of a try-catch block. This is all
// implemented in FinallyInfo. Here we enter a new EHCatchScope.
FinallyInfo.enter(CGF, Finally->getFinallyBody(), beginCatchFn,
endCatchFn, exceptionRethrowFn);
- } else if (IsWasm) {
- CGF.ErrorUnsupported(Finally,
- "@finally is not implemented for WebAssembly");
- } else if (IsMSVC) {
+ } else {
CodeGenFunction HelperCGF(CGM, /*suppressNewContext=*/true);
if (!CGF.CurSEHParent)
CGF.CurSEHParent = cast<NamedDecl>(CGF.CurFuncDecl);
@@ -224,6 +220,7 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
}
// We save the old funclet pad here before we traverse each catch handler.
+ llvm::Instruction *SavedFuncletPad = CGF.CurrentFuncletPad;
SaveAndRestore RestoreCurrentFuncletPad(CGF.CurrentFuncletPad);
llvm::BasicBlock *WasmCatchStartBlock = nullptr;
llvm::CatchPadInst *CPI = nullptr;
@@ -312,8 +309,10 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
CGF.Builder.restoreIP(SavedIP);
// Pop out of the finally.
- if (!useFunclets && S.getFinallyStmt())
+ if (!IsMSVC && S.getFinallyStmt()) {
+ CGF.CurrentFuncletPad = SavedFuncletPad;
FinallyInfo.exit(CGF);
+ }
if (Cont.isValid())
CGF.EmitBlock(Cont.getBlock());
diff --git a/clang/test/CodeGenObjC/gnustep2-wasm32-finally.m b/clang/test/CodeGenObjC/gnustep2-wasm32-finally.m
new file mode 100644
index 0000000000000..c92945e90ba1f
--- /dev/null
+++ b/clang/test/CodeGenObjC/gnustep2-wasm32-finally.m
@@ -0,0 +1,204 @@
+// REQUIRES: webassembly-registered-target
+// RUN: %clang_cc1 -target-feature +exception-handling -triple wasm32-unknown-emscripten -fobjc-runtime=gnustep-2.2 -fexceptions -fobjc-exceptions -exception-model=wasm -mllvm -wasm-enable-eh -emit-llvm -o - %s | FileCheck %s
+
+__attribute__((objc_root_class)) @interface Object @end
+extern void mayThrowObjC();
+
+void emptyFinally(void) {
+ @try {
+ mayThrowObjC();
+ } @finally {
+ }
+}
+
+// CHECK-LABEL: define{{.*}} @emptyFinally
+// CHECK: invoke void @mayThrowObjC()
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: cleanup:
+// CHECK: br i1 {{.*}}, label %finally.rethrow, label %finally.cont
+// CHECK: finally.rethrow:
+// CHECK: invoke void @__cxa_rethrow()
+// CHECK: finally.catchall:
+// CHECK: catchret from
+
+int finallySimple(Object *object) {
+ int value = 0;
+ @try {
+ mayThrowObjC();
+ value = 1;
+ } @catch (...) {
+ value = 2;
+ } @finally {
+ value += object != (Object *)0;
+ }
+ return value;
+}
+
+// CHECK-LABEL: define{{.*}} @finallySimple
+// CHECK: invoke void @mayThrowObjC()
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: cleanup:
+// CHECK: br i1 {{.*}}, label %finally.rethrow, label %finally.cont
+// CHECK: finally.rethrow:
+// CHECK: invoke void @__cxa_rethrow()
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: finally.catchall:
+// CHECK: catchret from
+
+int finallyNoCatch(Object *object) {
+ int value = 0;
+ @try {
+ mayThrowObjC();
+ value = 1;
+ } @finally {
+ value += object != (Object *)0;
+ }
+ return value;
+}
+
+// CHECK-LABEL: define{{.*}} @finallyNoCatch
+// CHECK: invoke void @mayThrowObjC()
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: cleanup:
+// CHECK: br i1 {{.*}}, label %finally.rethrow, label %finally.cont
+// CHECK: finally.rethrow:
+// CHECK: invoke void @__cxa_rethrow()
+// CHECK: finally.catchall:
+// CHECK: catchret from
+
+int throwInTryFinally(Object *object) {
+ int value = 0;
+ @try {
+ @throw (id)0;
+ } @catch (...) {
+ value = 1;
+ } @finally {
+ value += object != (Object *)0;
+ }
+ return value;
+}
+
+// CHECK-LABEL: define{{.*}} @throwInTryFinally
+// CHECK: invoke void @objc_exception_throw
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: finally.catchall:
+// CHECK: catchret from
+// CHECK: cleanup:
+// CHECK: br i1 {{.*}}, label %finally.rethrow, label %finally.cont
+
+int throwInCatchFinally(Object *object) {
+ @try {
+ mayThrowObjC();
+ } @catch (...) {
+ @throw;
+ } @finally {
+ (void)object;
+ }
+}
+
+// CHECK-LABEL: define{{.*}} @throwInCatchFinally
+// CHECK: invoke void @mayThrowObjC()
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: catch:
+// CHECK: invoke void @__cxa_rethrow()
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: finally.catchall:
+
+int throwInFinally(Object *object) {
+ @try {
+ mayThrowObjC();
+ } @finally {
+ @throw object;
+ }
+}
+
+// CHECK-LABEL: define{{.*}} @throwInFinally
+// CHECK: invoke void @mayThrowObjC()
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: cleanup:
+// CHECK: invoke void @objc_exception_throw(ptr
+// CHECK-NEXT: to label %{{.*}} unwind label %ehcleanup
+
+int throwInFinallyNoException(Object *object) {
+ @try {
+ } @finally {
+ @throw object;
+ }
+}
+
+// CHECK-LABEL: define{{.*}} @throwInFinallyNoException
+// CHECK: invoke void @objc_exception_throw(ptr
+// CHECK-NEXT: to label %{{.*}} unwind label %ehcleanup
+// CHECK-NOT: catchswitch within none
+
+int nestedTryCatchFinally(Object *object) {
+ int value = 0;
+ @try {
+ @try {
+ mayThrowObjC();
+ } @catch (...) {
+ value = 1;
+ } @finally {
+ value += 2;
+ }
+ } @catch (...) {
+ value = 3;
+ } @finally {
+ value += object != (Object *)0;
+ }
+ return value;
+}
+
+// CHECK-LABEL: define{{.*}} @nestedTryCatchFinally
+// CHECK: invoke void @mayThrowObjC()
+// CHECK: catchswitch within none
+// CHECK: catchswitch within none
+// CHECK: finally.catchall:
+// CHECK: finally.catchall{{.*}}:
+
+int gotoOutFinally(Object *object) {
+ int value = 0;
+ @try {
+ value = 1;
+ goto done;
+ } @finally {
+ value += object != (Object *)0;
+ }
+done:
+ return value;
+}
+
+// CHECK-LABEL: define{{.*}} @gotoOutFinally
+// CHECK: store i32 1, ptr %value
+// CHECK: store i32 3, ptr %cleanup.dest.slot
+// CHECK: br i1 {{.*}}, label %finally.rethrow, label %finally.cont
+// CHECK: ret i32
+
+int gotoOutCatchFinally(Object *object) {
+ int value = 0;
+ @try {
+ mayThrowObjC();
+ } @catch (...) {
+ value = 1;
+ goto done;
+ } @finally {
+ value += object != (Object *)0;
+ }
+done:
+ return value;
+}
+
+// CHECK-LABEL: define{{.*}} @gotoOutCatchFinally
+// CHECK: catchswitch within none
+// CHECK: catchret from
+// CHECK: br label %cleanup
diff --git a/clang/test/CodeGenObjCXX/wasm32-eh-objcxx.mm b/clang/test/CodeGenObjCXX/wasm32-eh-objcxx.mm
index 3af09f7055b98..e11a27afb547d 100644
--- a/clang/test/CodeGenObjCXX/wasm32-eh-objcxx.mm
+++ b/clang/test/CodeGenObjCXX/wasm32-eh-objcxx.mm
@@ -33,6 +33,43 @@ int cxxDestructorsAroundCatch() {
extern void mayThrowObjC();
+int cleanupInTryFinally() {
+ @try {
+ ThrowingDestructor object;
+ mayThrowObjC();
+ } @finally {
+ }
+ return 0;
+}
+
+// CHECK-LABEL: define{{.*}} @_Z{{[0-9]+}}cleanupInTryFinallyv
+// CHECK: invoke void @_Z{{[0-9]+}}mayThrowObjCv()
+// CHECK: [[TRY_CLEANUP:%.*]] = cleanuppad within
+// CHECK: invoke{{.*}} @_ZN18ThrowingDestructorD1Ev{{.*}}[ "funclet"(token [[TRY_CLEANUP]]) ]
+// CHECK: cleanupret from [[TRY_CLEANUP]]
+// CHECK: catchswitch within none
+// CHECK: catchret from
+
+int cleanupInCatchFinally() {
+ @try {
+ mayThrowObjC();
+ } @catch (...) {
+ ThrowingDestructor object;
+ return 1;
+ } @finally {
+ }
+ return 0;
+}
+
+// CHECK-LABEL: define{{.*}} @_Z{{[0-9]+}}cleanupInCatchFinallyv
+// CHECK: invoke void @_Z{{[0-9]+}}mayThrowObjCv()
+// CHECK: [[CATCHPAD:%.*]] = catchpad within
+// CHECK: invoke{{.*}} @_ZN18ThrowingDestructorD1Ev{{.*}}[ "funclet"(token [[CATCHPAD]]) ]
+// CHECK: catchret from [[CATCHPAD]]
+// CHECK: catchswitch within none
+// CHECK: catchpad within
+// CHECK: finally.catchall:
+
int combinedCxxObjcEH() {
@try {
try {
More information about the cfe-commits
mailing list