Skip to content

Conversation

@HendrikHuebner
Copy link
Contributor

@HendrikHuebner HendrikHuebner commented Dec 7, 2025

Related to #169043 and part of a broader effort to support targetting WebAssembly for ObjectiveC.

This PR is work in progress and adds basic support for generating funclet-style exception handling (try/catch) for WebAssembly. For now, I've set the gxx_wasm personality function. I'm not entirely sure whether we need to define out own.

For these examples, the generated IR matches the IR generated from an equivalent C++ program (Aside from the exception type ID).

extern void might_throw(void);

void test_simple_try_catch(void) {
  @try {
    might_throw();
  } @catch (id e) {
  } @catch (id e) {
  } 
}

void test_catch_specific_type(void) {
  @try {
    might_throw();
  } @catch (id e) {
  }
}

void test_nested_try_catch(void) {
  @try {
    @try {
      might_throw();
    } @catch (id e) {
    }
  } @catch (id e) {
  }
}

@HendrikHuebner
Copy link
Contributor Author

cc @hmelder

// Remember where we were.
CGBuilderTy::InsertPoint SavedIP = CGF.Builder.saveAndClearIP();

// Wasm uses Windows-style EH instructions, but merges all catch clauses into
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: This is taken from CGException.cpp, we may want to refactor it into a shared helper.

@github-actions
Copy link

github-actions bot commented Dec 7, 2025

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff origin/main HEAD --extensions h,cpp -- clang/lib/CodeGen/CGCleanup.h clang/lib/CodeGen/CGException.cpp clang/lib/CodeGen/CGObjCRuntime.cpp clang/lib/CodeGen/CodeGenFunction.h clang/lib/Driver/ToolChains/Clang.cpp --diff_from_common_commit

⚠️
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing origin/main to the base branch/commit you want to compare against.
⚠️

View the diff from clang-format here.
diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp
index aa4622695..4d3bf1c1e 100644
--- a/clang/lib/CodeGen/CGObjCRuntime.cpp
+++ b/clang/lib/CodeGen/CGObjCRuntime.cpp
@@ -157,8 +157,8 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
   CodeGenFunction::FinallyInfo FinallyInfo;
   if (!useFunclets)
     if (const ObjCAtFinallyStmt *Finally = S.getFinallyStmt())
-      FinallyInfo.enter(CGF, Finally->getFinallyBody(),
-                        beginCatchFn, endCatchFn, exceptionRethrowFn);
+      FinallyInfo.enter(CGF, Finally->getFinallyBody(), beginCatchFn,
+                        endCatchFn, exceptionRethrowFn);
   SmallVector<CatchHandler, 8> Handlers;
 
 
@@ -193,9 +193,9 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
 
   if (useFunclets) {
     if (const ObjCAtFinallyStmt *Finally = S.getFinallyStmt()) {
-        if (hasWasmExceptions) {
-          CGF.ErrorUnsupported(Finally, "@finally for WASM");
-        }
+      if (hasWasmExceptions) {
+        CGF.ErrorUnsupported(Finally, "@finally for WASM");
+      }
 
         CodeGenFunction HelperCGF(CGM, /*suppressNewContext=*/true);
         if (!CGF.CurSEHParent)
@@ -217,12 +217,11 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
     }
   }
 
-
   // Emit the try body.
   CGF.EmitStmt(S.getTryBody());
 
   // Leave the try.
-  llvm::BasicBlock* dispatchBlock{};
+  llvm::BasicBlock *dispatchBlock{};
   if (S.getNumCatchStmts())
     dispatchBlock = CGF.popCatchScope();
 
@@ -234,14 +233,15 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
   // each catch handler.
   SaveAndRestore RestoreCurrentFuncletPad(CGF.CurrentFuncletPad);
   llvm::BasicBlock *WasmCatchStartBlock = nullptr;
-  llvm::CatchPadInst* CatchPadInst{};
+  llvm::CatchPadInst *CatchPadInst{};
   if (!!dispatchBlock && hasWasmExceptions) {
     auto *CatchSwitch =
         cast<llvm::CatchSwitchInst>(dispatchBlock->getFirstNonPHIIt());
     WasmCatchStartBlock = CatchSwitch->hasUnwindDest()
                               ? CatchSwitch->getSuccessor(1)
                               : CatchSwitch->getSuccessor(0);
-    CatchPadInst = cast<llvm::CatchPadInst>(WasmCatchStartBlock->getFirstNonPHIIt());
+    CatchPadInst =
+        cast<llvm::CatchPadInst>(WasmCatchStartBlock->getFirstNonPHIIt());
     CGF.CurrentFuncletPad = CatchPadInst;
   }
 
@@ -258,9 +258,11 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
       llvm::BasicBlock::iterator CPICandidate =
           Handler.Block->getFirstNonPHIIt();
       if (CPICandidate != Handler.Block->end()) {
-        if ((CatchPadInst = dyn_cast_or_null<llvm::CatchPadInst>(CPICandidate))) {
+        if ((CatchPadInst =
+                 dyn_cast_or_null<llvm::CatchPadInst>(CPICandidate))) {
           CGF.CurrentFuncletPad = CatchPadInst;
-          CatchPadInst->setOperand(2, CGF.getExceptionSlot().emitRawPointer(CGF));
+          CatchPadInst->setOperand(2,
+                                   CGF.getExceptionSlot().emitRawPointer(CGF));
         }
       }
     }
@@ -318,7 +320,7 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF,
     assert(RethrowBlock != WasmCatchStartBlock && RethrowBlock->empty());
     CGF.Builder.SetInsertPoint(RethrowBlock);
     llvm::Function *RethrowInCatchFn =
-      CGM.getIntrinsic(llvm::Intrinsic::wasm_rethrow);
+        CGM.getIntrinsic(llvm::Intrinsic::wasm_rethrow);
     CGF.EmitNoreturnRuntimeCallOrInvoke(RethrowInCatchFn, {});
   }
 
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index 86c02f0ac..b284f58bc 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -1307,7 +1307,7 @@ public:
   /// popCatchScope - Pops the catch scope at the top of the EHScope
   /// stack, emitting any required code (other than the catch handlers
   /// themselves).
-  llvm::BasicBlock* popCatchScope();
+  llvm::BasicBlock *popCatchScope();
 
   llvm::BasicBlock *getEHResumeBlock(bool isCleanup);
   llvm::BasicBlock *getEHDispatchBlock(EHScopeStack::stable_iterator scope);

@hmelder
Copy link
Contributor

hmelder commented Dec 7, 2025

Thank you!

cc @davidchisnall

we likely need to add a new one to handle ObjectiveC's @finally semantics.

This is currently also broken on Windows (MSVC ABI) so we might be able to fix this one as well.

For now, I've set the gxx_wasm personality function

I'll test your patch tomorrow and adapt the personality function in libobjc2 if needed.

@hmelder
Copy link
Contributor

hmelder commented Dec 7, 2025

Given that we emit the same IR instructions for EH on Windows.

@HendrikHuebner
Copy link
Contributor Author

HendrikHuebner commented Dec 8, 2025

Thank you!

cc @davidchisnall

we likely need to add a new one to handle ObjectiveC's @finally semantics.

This is currently also broken on Windows (MSVC ABI) so we might be able to fix this one as well.

For now, I've set the gxx_wasm personality function

I'll test your patch tomorrow and adapt the personality function in libobjc2 if needed.

I'm not sure if the personality function is going to be a concern. The WebAssembly VM should handle the unwinding process (see here). However, other runtime function may need to be adapted in libobjc2, as they were for C++.

@triplef
Copy link
Member

triplef commented Dec 8, 2025

Just for context: the issue with @finally on Windows is tracked in #43828 and #51899.

@hmelder
Copy link
Contributor

hmelder commented Dec 8, 2025

extern void might_throw(void);

void test_simple_try_catch(void) {
  @try {
    might_throw();
  } @catch (id e) {
  } @catch (id e) {
  } 
}

I compiled your example with -target wasm32-unknown-emscripten -fwasm-exceptions -mllvm -wasm-use-legacy-eh=false -fobjc-runtime=gnustep-2.2 -S -emit-llvm and the generated IR looks good.

The generated IR
; Function Attrs: noinline optnone
define hidden void @test_simple_try_catch() #0 personality ptr @__gxx_wasm_personality_v0 {
entry:
  %exn.slot = alloca ptr, align 4
  %e = alloca ptr, align 4
  %e5 = alloca ptr, align 4
  invoke void @might_throw()
          to label %invoke.cont unwind label %catch.dispatch

catch.dispatch:                                   ; preds = %entry
  %0 = catchswitch within none [label %catch.start] unwind to caller

catch.start:                                      ; preds = %catch.dispatch
  %1 = catchpad within %0 [ptr @0, ptr @0]
  %2 = call ptr @llvm.wasm.get.exception(token %1)
  store ptr %2, ptr %exn.slot, align 4
  %3 = call i32 @llvm.wasm.get.ehselector(token %1)
  %4 = call i32 @llvm.eh.typeid.for.p0(ptr @0) #5
  %matches = icmp eq i32 %3, %4
  br i1 %matches, label %catch, label %catch.fallthrough

catch.fallthrough:                                ; preds = %catch.start
  %5 = call i32 @llvm.eh.typeid.for.p0(ptr @0) #5
  %matches1 = icmp eq i32 %3, %5
  br i1 %matches1, label %catch2, label %rethrow

rethrow:                                          ; preds = %catch.fallthrough
  call void @llvm.wasm.rethrow() #4 [ "funclet"(token %1) ]
  unreachable

invoke.cont:                                      ; preds = %entry
  br label %eh.cont

eh.cont:                                          ; preds = %invoke.cont, %catchret.dest6, %catchret.dest
  ret void

catch:                                            ; preds = %catch.start
  %exn = load ptr, ptr %exn.slot, align 4
  %exn.adjusted = call ptr @objc_begin_catch(ptr %exn) #5 [ "funclet"(token %1) ]
  store ptr %exn.adjusted, ptr %e, align 4
  call void @objc_end_catch() #5 [ "funclet"(token %1) ]
  catchret from %1 to label %catchret.dest

catchret.dest:                                    ; preds = %catch
  br label %eh.cont

catch2:                                           ; preds = %catch.fallthrough
  %exn3 = load ptr, ptr %exn.slot, align 4
  %exn.adjusted4 = call ptr @objc_begin_catch(ptr %exn3) #5 [ "funclet"(token %1) ]
  store ptr %exn.adjusted4, ptr %e5, align 4
  call void @objc_end_catch() #5 [ "funclet"(token %1) ]
  catchret from %1 to label %catchret.dest6

catchret.dest6:                                   ; preds = %catch2
  br label %eh.cont
}

@hmelder
Copy link
Contributor

hmelder commented Dec 8, 2025

I'm not sure if the personality function is going to be a concern.

AFAIK it is. On WASM, there is only one Unwind phase instead of a search and install phase.

@hmelder
Copy link
Contributor

hmelder commented Dec 8, 2025

I'm currently hacking in support for the ObjC personality function (crimes are being commited) and it looks like __gxx_wasm_personality_v0 is hardcoded in the backend. For example WasmEHPrepare.cpp:244.

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.

3 participants