Skip to content

[CodeGen][ObjC] Try/Catch support for WebAssembly - #171038

Closed
HendrikHuebner wants to merge 1 commit into
llvm:mainfrom
HendrikHuebner:wasm
Closed

[CodeGen][ObjC] Try/Catch support for WebAssembly#171038
HendrikHuebner wants to merge 1 commit into
llvm:mainfrom
HendrikHuebner:wasm

Conversation

@HendrikHuebner

@HendrikHuebner HendrikHuebner commented Dec 7, 2025

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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.

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.

+1 on finding a way to reuse the same code in CGException.cpp. I'm not familiar with his CGObjRuntime.cpp, but can't ObjC (at least partly) use functions in CGException.cpp? What's the reason ObjC maintains separate EH handling routines?

@github-actions

github-actions Bot commented Dec 7, 2025

Copy link
Copy Markdown

⚠️ 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 cpp,h -- 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 dcff49453..bdb59b1c9 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 660b33284..096b1c0cd 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -1326,7 +1326,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

hmelder commented Dec 7, 2025

Copy link
Copy Markdown
Contributor

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

hmelder commented Dec 7, 2025

Copy link
Copy Markdown
Contributor

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

@HendrikHuebner

HendrikHuebner commented Dec 8, 2025

Copy link
Copy Markdown
Contributor Author

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

triplef commented Dec 8, 2025

Copy link
Copy Markdown
Member

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

@hmelder

hmelder commented Dec 8, 2025

Copy link
Copy Markdown
Contributor
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

hmelder commented Dec 8, 2025

Copy link
Copy Markdown
Contributor

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

hmelder commented Dec 8, 2025

Copy link
Copy Markdown
Contributor

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.

@aheejin aheejin left a comment

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.

It looks you have code for catch handling but for cleanups (= destructors). Do you need to handle them as well?

static const EHPersonality GNU_ObjC_SJLJ;
static const EHPersonality GNU_ObjC_SEH;
static const EHPersonality GNUstep_ObjC;
static const EHPersonality GNUstep_Wasm_ObjC;

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.

This doesn't seem to be used?

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

// Wasm uses Windows-style EH instructions, but merges all catch clauses into

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.

+1 on finding a way to reuse the same code in CGException.cpp. I'm not familiar with his CGObjRuntime.cpp, but can't ObjC (at least partly) use functions in CGException.cpp? What's the reason ObjC maintains separate EH handling routines?

@github-actions

Copy link
Copy Markdown

🐧 Linux x64 Test Results

The build failed before running any tests. Click on a failure below to see the details.

tools/clang/lib/CodeGen/CMakeFiles/obj.clangCodeGen.dir/CGObjCRuntime.cpp.o
FAILED: tools/clang/lib/CodeGen/CMakeFiles/obj.clangCodeGen.dir/CGObjCRuntime.cpp.o
sccache /opt/llvm/bin/clang++ -DCLANG_EXPORTS -D_DEBUG -D_GLIBCXX_ASSERTIONS -D_GLIBCXX_USE_CXX11_ABI=1 -D_GNU_SOURCE -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -I/home/gha/actions-runner/_work/llvm-project/llvm-project/build/tools/clang/lib/CodeGen -I/home/gha/actions-runner/_work/llvm-project/llvm-project/clang/lib/CodeGen -I/home/gha/actions-runner/_work/llvm-project/llvm-project/clang/include -I/home/gha/actions-runner/_work/llvm-project/llvm-project/build/tools/clang/include -I/home/gha/actions-runner/_work/llvm-project/llvm-project/build/include -I/home/gha/actions-runner/_work/llvm-project/llvm-project/llvm/include -gmlt -fPIC -fno-semantic-interposition -fvisibility-inlines-hidden -Werror -Werror=date-time -Werror=unguarded-availability-new -Wall -Wextra -Wno-unused-parameter -Wwrite-strings -Wcast-qual -Wmissing-field-initializers -pedantic -Wno-long-long -Wc++98-compat-extra-semi -Wimplicit-fallthrough -Wcovered-switch-default -Wno-noexcept-type -Wnon-virtual-dtor -Wdelete-non-virtual-dtor -Wsuggest-override -Wstring-conversion -Wno-pass-failed -Wmisleading-indentation -Wctad-maybe-unsupported -fdiagnostics-color -ffunction-sections -fdata-sections -fno-common -Woverloaded-virtual -Wno-nested-anon-types -O3 -DNDEBUG -std=c++17 -UNDEBUG -fno-exceptions -funwind-tables -fno-rtti -MD -MT tools/clang/lib/CodeGen/CMakeFiles/obj.clangCodeGen.dir/CGObjCRuntime.cpp.o -MF tools/clang/lib/CodeGen/CMakeFiles/obj.clangCodeGen.dir/CGObjCRuntime.cpp.o.d -o tools/clang/lib/CodeGen/CMakeFiles/obj.clangCodeGen.dir/CGObjCRuntime.cpp.o -c /home/gha/actions-runner/_work/llvm-project/llvm-project/clang/lib/CodeGen/CGObjCRuntime.cpp
/home/gha/actions-runner/_work/llvm-project/llvm-project/clang/lib/CodeGen/CGObjCRuntime.cpp:314:29: error: no member named 'BranchInst' in namespace 'llvm'
314 |       auto *BI = cast<llvm::BranchInst>(TI);
|                             ^~~~~~~~~~
1 error generated.

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

@github-actions

Copy link
Copy Markdown

🪟 Windows x64 Test Results

The build failed before running any tests. Click on a failure below to see the details.

[code=1] tools/clang/lib/CodeGen/CMakeFiles/obj.clangCodeGen.dir/CGObjCRuntime.cpp.obj
FAILED: [code=1] tools/clang/lib/CodeGen/CMakeFiles/obj.clangCodeGen.dir/CGObjCRuntime.cpp.obj
sccache C:\clang\clang-msvc\bin\clang-cl.exe  /nologo -TP -DCLANG_BUILD_STATIC -DUNICODE -D_CRT_NONSTDC_NO_DEPRECATE -D_CRT_NONSTDC_NO_WARNINGS -D_CRT_SECURE_NO_DEPRECATE -D_CRT_SECURE_NO_WARNINGS -D_GLIBCXX_ASSERTIONS -D_HAS_EXCEPTIONS=0 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE -D_SCL_SECURE_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS -D_UNICODE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -IC:\_work\llvm-project\llvm-project\build\tools\clang\lib\CodeGen -IC:\_work\llvm-project\llvm-project\clang\lib\CodeGen -IC:\_work\llvm-project\llvm-project\clang\include -IC:\_work\llvm-project\llvm-project\build\tools\clang\include -IC:\_work\llvm-project\llvm-project\build\include -IC:\_work\llvm-project\llvm-project\llvm\include /DWIN32 /D_WINDOWS   /Zc:inline /Zc:__cplusplus /Oi /Brepro /bigobj /permissive- -Werror=unguarded-availability-new /W4  -Wextra -Wno-unused-parameter -Wwrite-strings -Wcast-qual -Wmissing-field-initializers -Wimplicit-fallthrough -Wcovered-switch-default -Wno-noexcept-type -Wnon-virtual-dtor -Wdelete-non-virtual-dtor -Wsuggest-override -Wstring-conversion -Wno-pass-failed -Wmisleading-indentation -Wctad-maybe-unsupported /Gw /O2 /Ob2  -std:c++17 -MD -UNDEBUG /EHs-c- /GR- /showIncludes /Fotools\clang\lib\CodeGen\CMakeFiles\obj.clangCodeGen.dir\CGObjCRuntime.cpp.obj /Fdtools\clang\lib\CodeGen\CMakeFiles\obj.clangCodeGen.dir\ -c -- C:\_work\llvm-project\llvm-project\clang\lib\CodeGen\CGObjCRuntime.cpp
C:\_work\llvm-project\llvm-project\clang\lib\CodeGen\CGObjCRuntime.cpp(314,29): error: no member named 'BranchInst' in namespace 'llvm'
314 |       auto *BI = cast<llvm::BranchInst>(TI);
|                             ^~~~~~~~~~
1 error generated.

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

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