https://github.com/hmelder created https://github.com/llvm/llvm-project/pull/183753
This PR adds initial support for Objective-C on WebAssembly with C++-based EH. It does not include my draft for `@finally`, as I want to discuss the design of it first (see below). A couple of changes were required in libobjc2 for RTTI and compiler flags. This will be tracked seperately. Here is an overview of the changes. The first two items were already discussed in https://github.com/llvm/llvm-project/pull/169043 - Mangle public symbols with '$' instead of '.' as the latter is not a valid javascript identifier. - Fix the function signature of `class_registerAlias_np` to return a bool instead of void. - Use C++-based exceptions for Wasm and the GNUstep runtime ## Exception Handling Implementation On most supported platforms, libobjc2 uses its own personality function and libunwind instead of piggybacking on C++ exceptions. The Wasm EH implementation in Clang hard-codes `__gxx_wasm_personality_v0` by calling the veneer function `_Unwind_CallPersonality` instead of calling the personality function directly. While it is possible to remove `_Unwind_CallPersonality` (original approach https://github.com/llvm/llvm-project/pull/175202) and instead generate its body in codegen, this will add a couple of instructions in each catch block. Doable, but it is easier to just use C++-based EH. ### `@finally` support `@finally` requires a cleanup for the normal and exceptional control flow. For the latter, we create a new EHCatchScope with one catch-all handler on platforms with landing pads (See [CGException.cpp#L1440](https://github.com/llvm/llvm-project/blob/7402312ae12d91df436486ca296119607a81edd0/clang/lib/CodeGen/CGException.cpp#L1440)). This essentially corresponds to the following in C++ (https://godbolt.org/z/TannYWda5): ```c++ // The catch-all scope for @finally try { // The original try-catch block try { // ... } catch (int i) { num = 42; } // finally cleanup on normal edge } catch (...) { // finally cleanup on exceptional edge } ``` The current `FinallyInfo` code was written for `landingpad`s and requires some modification. I'd like some input on this, but I envision something like this: In FinallyInfo::enter 1. Prepare dispatchBlock. This is a special case with only one catch-all handler, so use finally.catchall just like in CGException.cpp 2. emitWasmCatchPadBlock() 3. Update CurrentFuncletPad = ... The catchswitch from the actual try block then unwinds to the finally catchswitch. >From 38fb99534154f37f43a69318407550db2dbe45f5 Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Fri, 21 Nov 2025 10:42:47 +0000 Subject: [PATCH 01/12] [clang] Whitelist wasm when targeting GNUstep 2.x --- clang/lib/Driver/ToolChains/Clang.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index ab671d032644b..6f583af41999e 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -8091,7 +8091,8 @@ ObjCRuntime Clang::AddObjCRuntimeArgs(const ArgList &args, if ((runtime.getKind() == ObjCRuntime::GNUstep) && (runtime.getVersion() >= VersionTuple(2, 0))) if (!getToolChain().getTriple().isOSBinFormatELF() && - !getToolChain().getTriple().isOSBinFormatCOFF()) { + !getToolChain().getTriple().isOSBinFormatCOFF() && + !getToolChain().getTriple().isOSBinFormatWasm()) { getToolChain().getDriver().Diag( diag::err_drv_gnustep_objc_runtime_incompatible_binary) << runtime.getVersion().getMajor(); >From 6e135cdab416fb5c03ab3f78a61a575a1a6644ed Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Fri, 21 Nov 2025 10:50:35 +0000 Subject: [PATCH 02/12] [CodeGen][ObjC] Mangle public symbols for wasm Emscripten requires that exported symbols. See https://github.com/emscripten-core/emscripten/pull/23563. --- clang/lib/CodeGen/CGObjCGNU.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCGNU.cpp b/clang/lib/CodeGen/CGObjCGNU.cpp index 32fd6b760ed11..698fe16df7b85 100644 --- a/clang/lib/CodeGen/CGObjCGNU.cpp +++ b/clang/lib/CodeGen/CGObjCGNU.cpp @@ -179,8 +179,16 @@ class CGObjCGNU : public CGObjCRuntime { (R.getVersion() >= VersionTuple(major, minor)); } - std::string ManglePublicSymbol(StringRef Name) { - return (StringRef(CGM.getTriple().isOSBinFormatCOFF() ? "$_" : "._") + Name).str(); + const std::string ManglePublicSymbol(StringRef Name) { + StringRef prefix = "._"; + + // Exported symbols in Emscripten must be a valid Javascript identifier. + auto triple = CGM.getTriple(); + if (triple.isOSBinFormatCOFF() || triple.isOSBinFormatWasm()) { + prefix = "$_"; + } + + return (prefix + Name).str(); } std::string SymbolForProtocol(Twine Name) { >From 90ce962fd0b450eef7670fc2ad07165cff2a555a Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Fri, 21 Nov 2025 13:04:33 +0000 Subject: [PATCH 03/12] [CodeGen][ObjC] Fix class_registerAlias_np signature --- clang/lib/CodeGen/CGObjCGNU.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCGNU.cpp b/clang/lib/CodeGen/CGObjCGNU.cpp index 698fe16df7b85..080b263e5fd4d 100644 --- a/clang/lib/CodeGen/CGObjCGNU.cpp +++ b/clang/lib/CodeGen/CGObjCGNU.cpp @@ -4119,8 +4119,7 @@ llvm::Function *CGObjCGNU::ModuleInitFunction() { if (!ClassAliases.empty()) { llvm::Type *ArgTypes[2] = {PtrTy, PtrToInt8Ty}; llvm::FunctionType *RegisterAliasTy = - llvm::FunctionType::get(Builder.getVoidTy(), - ArgTypes, false); + llvm::FunctionType::get(BoolTy, ArgTypes, false); llvm::Function *RegisterAlias = llvm::Function::Create( RegisterAliasTy, llvm::GlobalValue::ExternalWeakLinkage, "class_registerAlias_np", >From 2a9692a67daf099e97d10daa3c5e7c691717e81f Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Fri, 28 Nov 2025 16:28:35 +0000 Subject: [PATCH 04/12] [CodeGen][ObjC] Add WASM symbol mangling test --- .../CodeGenObjC/gnustep2-wasm32-symbols.m | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 clang/test/CodeGenObjC/gnustep2-wasm32-symbols.m diff --git a/clang/test/CodeGenObjC/gnustep2-wasm32-symbols.m b/clang/test/CodeGenObjC/gnustep2-wasm32-symbols.m new file mode 100644 index 0000000000000..7da73b8f1903e --- /dev/null +++ b/clang/test/CodeGenObjC/gnustep2-wasm32-symbols.m @@ -0,0 +1,23 @@ +// RUN: %clang_cc1 -triple wasm32-unknown-emscripten -emit-llvm -fobjc-runtime=gnustep-2.2 -o - %s | FileCheck %s + +@class NSString; + +@protocol AProtocol +- (void) meth; +@end + +@interface AClass <AProtocol> +@end + +@implementation AClass +- (void) meth {} +@end + +// Make sure that all public symbols are mangled correctly. All exported symbols +// must be valid Javascript identifiers in Emscripten. +// CHECK: $"$_OBJC_PROTOCOL_AProtocol" = comdat any +// CHECK: @"$_OBJC_METACLASS_AClass" +// CHECK: @"$_OBJC_PROTOCOL_AProtocol" +// CHECK: @"$_OBJC_CLASS_AClass" +// CHECK: @"$_OBJC_REF_CLASS_AClass" +// CHECK: @"$_OBJC_INIT_CLASS_AClass" >From 912f59824d1b4ec4b96fbe85c62d8782e1f3d516 Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Wed, 14 Jan 2026 10:23:18 +0000 Subject: [PATCH 05/12] [CodeGen][ObjC] Use C++-based EH for WASM targets The Wasm EH implementation in Clang pretty much hard-codes __gxx_wasm_personality_v0 by calling the veneer function _Unwind_CallPersonality instead of calling the personality function directly. While it is possible to remove _Unwind_CallPersonality and instead generate its body in CG, this will add a couple of instructions in each catch block. Doable, but we can also do the following: Since we already have C++-based EH for MinGW in CGObjCGNU, reusing it for Wasm saves us from implementing our own personality function and objc_begin_catch/objc_end_catch functions. --- clang/lib/CodeGen/CGException.cpp | 13 ++++++++++--- clang/lib/CodeGen/CGObjCGNU.cpp | 9 ++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/clang/lib/CodeGen/CGException.cpp b/clang/lib/CodeGen/CGException.cpp index e9d20672ce185..749dde5a92dcd 100644 --- a/clang/lib/CodeGen/CGException.cpp +++ b/clang/lib/CodeGen/CGException.cpp @@ -161,6 +161,8 @@ static const EHPersonality &getObjCPersonality(const TargetInfo &Target, case ObjCRuntime::GNUstep: if (T.isOSCygMing()) return EHPersonality::GNU_CPlusPlus_SEH; + else if (T.isWasm()) + return EHPersonality::GNU_Wasm_CPlusPlus; else if (L.ObjCRuntime.getVersion() >= VersionTuple(1, 7)) return EHPersonality::GNUstep_ObjC; [[fallthrough]]; @@ -200,7 +202,8 @@ static const EHPersonality &getCXXPersonality(const TargetInfo &Target, static const EHPersonality &getObjCXXPersonality(const TargetInfo &Target, const CodeGenOptions &CGOpts, const LangOptions &L) { - if (Target.getTriple().isWindowsMSVCEnvironment()) + auto Triple = Target.getTriple(); + if (Triple.isWindowsMSVCEnvironment()) return EHPersonality::MSVC_CxxFrameHandler3; switch (L.ObjCRuntime.getKind()) { @@ -218,8 +221,12 @@ static const EHPersonality &getObjCXXPersonality(const TargetInfo &Target, return getObjCPersonality(Target, CGOpts, L); case ObjCRuntime::GNUstep: - return Target.getTriple().isOSCygMing() ? EHPersonality::GNU_CPlusPlus_SEH - : EHPersonality::GNU_ObjCXX; + if (Triple.isWasm()) + return EHPersonality::GNU_Wasm_CPlusPlus; + else if (Triple.isOSCygMing()) + return EHPersonality::GNU_CPlusPlus_SEH; + else + return EHPersonality::GNU_ObjCXX; // The GCC runtime's personality function inherently doesn't support // mixed EH. Use the ObjC personality just to avoid returning null. diff --git a/clang/lib/CodeGen/CGObjCGNU.cpp b/clang/lib/CodeGen/CGObjCGNU.cpp index 080b263e5fd4d..f9835703a68a9 100644 --- a/clang/lib/CodeGen/CGObjCGNU.cpp +++ b/clang/lib/CodeGen/CGObjCGNU.cpp @@ -2368,12 +2368,11 @@ CGObjCGNU::CGObjCGNU(CodeGenModule &cgm, unsigned runtimeABIVersion, MetaClassPtrAlias(nullptr), RuntimeVersion(runtimeABIVersion), ProtocolVersion(protocolClassVersion), ClassABIVersion(classABI) { + auto Triple = cgm.getContext().getTargetInfo().getTriple(); + msgSendMDKind = VMContext.getMDKindID("GNUObjCMessageSend"); - usesSEHExceptions = - cgm.getContext().getTargetInfo().getTriple().isWindowsMSVCEnvironment(); - usesCxxExceptions = - cgm.getContext().getTargetInfo().getTriple().isOSCygMing() && - isRuntime(ObjCRuntime::GNUstep, 2); + usesSEHExceptions = Triple.isWindowsMSVCEnvironment(); + usesCxxExceptions = (Triple.isOSCygMing() && isRuntime(ObjCRuntime::GNUstep, 2)) || Triple.isWasm(); CodeGenTypes &Types = CGM.getTypes(); IntTy = cast<llvm::IntegerType>( >From 21ce13271e3b275c30c27bbfa2892a407d49f021 Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Mon, 26 Jan 2026 08:02:32 +0000 Subject: [PATCH 06/12] [Clang][CodeGen] Return dispatch block when popping catch scope --- clang/lib/CodeGen/CGException.cpp | 4 +++- clang/lib/CodeGen/CodeGenFunction.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/clang/lib/CodeGen/CGException.cpp b/clang/lib/CodeGen/CGException.cpp index 749dde5a92dcd..0dbeed2b43d38 100644 --- a/clang/lib/CodeGen/CGException.cpp +++ b/clang/lib/CodeGen/CGException.cpp @@ -1210,11 +1210,13 @@ static void emitCatchDispatchBlock(CodeGenFunction &CGF, } } -void CodeGenFunction::popCatchScope() { +LLVM::BasicBlock *CodeGenFunction::popCatchScope() { EHCatchScope &catchScope = cast<EHCatchScope>(*EHStack.begin()); + LLVM::BasicBlock *dispatchBlock = catchScope.getCachedEHDispatchBlock(); if (catchScope.hasEHBranches()) emitCatchDispatchBlock(*this, catchScope); EHStack.popCatch(); + return dispatchBlock; } void CodeGenFunction::ExitCXXTryStmt(const CXXTryStmt &S, bool IsFnTryBlock) { diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h index 1073de1d25ec7..b44c8acda8488 100644 --- a/clang/lib/CodeGen/CodeGenFunction.h +++ b/clang/lib/CodeGen/CodeGenFunction.h @@ -1304,7 +1304,7 @@ class CodeGenFunction : public CodeGenTypeCache { /// popCatchScope - Pops the catch scope at the top of the EHScope /// stack, emitting any required code (other than the catch handlers /// themselves). - void popCatchScope(); + LLVM::BasicBlock *popCatchScope(); llvm::BasicBlock *getEHResumeBlock(bool isCleanup); llvm::BasicBlock *getEHDispatchBlock(EHScopeStack::stable_iterator scope); >From 44ded3da5093ccc83a00e53b9845b186eff913f5 Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Mon, 26 Jan 2026 10:48:09 +0000 Subject: [PATCH 07/12] [Clang] Create Wasm EH helper functions for ObjC EH --- clang/lib/CodeGen/CGException.cpp | 45 +++++++++++++++-------------- clang/lib/CodeGen/CodeGenFunction.h | 10 ++++++- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/clang/lib/CodeGen/CGException.cpp b/clang/lib/CodeGen/CGException.cpp index 0dbeed2b43d38..dbf09bc102ce9 100644 --- a/clang/lib/CodeGen/CGException.cpp +++ b/clang/lib/CodeGen/CGException.cpp @@ -1210,15 +1210,35 @@ static void emitCatchDispatchBlock(CodeGenFunction &CGF, } } -LLVM::BasicBlock *CodeGenFunction::popCatchScope() { +llvm::BasicBlock *CodeGenFunction::popCatchScope() { EHCatchScope &catchScope = cast<EHCatchScope>(*EHStack.begin()); - LLVM::BasicBlock *dispatchBlock = catchScope.getCachedEHDispatchBlock(); + llvm::BasicBlock *dispatchBlock = catchScope.getCachedEHDispatchBlock(); if (catchScope.hasEHBranches()) emitCatchDispatchBlock(*this, catchScope); EHStack.popCatch(); return dispatchBlock; } + +void CodeGenFunction::WasmEmitFallthroughRethrow(llvm::BasicBlock *WasmCatchStartBlock) { + assert(WasmCatchStartBlock); + // Navigate for the "rethrow" block. For CXX exceptions this was created in + // emitWasmCatchPadBlock(). Wasm uses landingpad-style conditional branches + // to compare selectors, so we follow the false destination for each of the + // cond branches to reach the rethrow block. + llvm::BasicBlock *RethrowBlock = WasmCatchStartBlock; + while (llvm::Instruction *TI = RethrowBlock->getTerminator()) { + auto *BI = cast<llvm::BranchInst>(TI); + assert(BI->isConditional()); + RethrowBlock = BI->getSuccessor(1); + } + assert(RethrowBlock != WasmCatchStartBlock && RethrowBlock->empty()); + Builder.SetInsertPoint(RethrowBlock); + llvm::Function *RethrowInCatchFn = + CGM.getIntrinsic(llvm::Intrinsic::wasm_rethrow); + EmitNoreturnRuntimeCallOrInvoke(RethrowInCatchFn, {}); +} + void CodeGenFunction::ExitCXXTryStmt(const CXXTryStmt &S, bool IsFnTryBlock) { unsigned NumHandlers = S.getNumHandlers(); EHCatchScope &CatchScope = cast<EHCatchScope>(*EHStack.begin()); @@ -1325,27 +1345,8 @@ void CodeGenFunction::ExitCXXTryStmt(const CXXTryStmt &S, bool IsFnTryBlock) { Builder.CreateBr(ContBB); } - // Because in wasm we merge all catch clauses into one big catchpad, in case - // none of the types in catch handlers matches after we test against each of - // them, we should unwind to the next EH enclosing scope. We generate a call - // to rethrow function here to do that. if (EHPersonality::get(*this).isWasmPersonality() && !HasCatchAll) { - assert(WasmCatchStartBlock); - // Navigate for the "rethrow" block we created in emitWasmCatchPadBlock(). - // Wasm uses landingpad-style conditional branches to compare selectors, so - // we follow the false destination for each of the cond branches to reach - // the rethrow block. - llvm::BasicBlock *RethrowBlock = WasmCatchStartBlock; - while (llvm::Instruction *TI = RethrowBlock->getTerminator()) { - auto *BI = cast<llvm::BranchInst>(TI); - assert(BI->isConditional()); - RethrowBlock = BI->getSuccessor(1); - } - assert(RethrowBlock != WasmCatchStartBlock && RethrowBlock->empty()); - Builder.SetInsertPoint(RethrowBlock); - llvm::Function *RethrowInCatchFn = - CGM.getIntrinsic(llvm::Intrinsic::wasm_rethrow); - EmitNoreturnRuntimeCallOrInvoke(RethrowInCatchFn, {}); + WasmEmitFallthroughRethrow(WasmCatchStartBlock); } EmitBlock(ContBB); diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h index b44c8acda8488..53e581cffc6ad 100644 --- a/clang/lib/CodeGen/CodeGenFunction.h +++ b/clang/lib/CodeGen/CodeGenFunction.h @@ -1304,7 +1304,15 @@ class CodeGenFunction : public CodeGenTypeCache { /// 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(); + + // This function should be called after emitting all catch clauses and none + // of them were 'catch-all' clauses. + // Because in wasm we merge all catch clauses into one big catchpad, in case + // none of the types in catch handlers matches after we test against each of + // them, we should unwind to the next EH enclosing scope. We generate a call + // to rethrow function here to do that. + void WasmEmitFallthroughRethrow(llvm::BasicBlock *WasmCatchStartBlock); llvm::BasicBlock *getEHResumeBlock(bool isCleanup); llvm::BasicBlock *getEHDispatchBlock(EHScopeStack::stable_iterator scope); >From 925137e31004624c45cee178cc1104614bd75d3c Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Mon, 26 Jan 2026 10:49:09 +0000 Subject: [PATCH 08/12] [ObjC] Support Wasm EH --- clang/lib/CodeGen/CGObjCRuntime.cpp | 47 +++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index 3d47dc9560c65..e33ad45fa061d 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// #include "CGObjCRuntime.h" +#include "Address.h" #include "CGCXXABI.h" #include "CGCleanup.h" #include "CGRecordLayout.h" @@ -23,6 +24,7 @@ #include "clang/CodeGen/CGFunctionInfo.h" #include "clang/CodeGen/CodeGenABITypes.h" #include "llvm/IR/Instruction.h" +#include "llvm/IR/Instructions.h" #include "llvm/Support/SaveAndRestore.h" using namespace clang; @@ -148,6 +150,8 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, 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 (!useFunclets) @@ -187,7 +191,7 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, Catch->setHandler(I, { Handlers[I].TypeInfo, Handlers[I].Flags }, Handlers[I].Block); } - if (useFunclets) + if (IsMSVC) if (const ObjCAtFinallyStmt *Finally = S.getFinallyStmt()) { CodeGenFunction HelperCGF(CGM, /*suppressNewContext=*/true); if (!CGF.CurSEHParent) @@ -213,30 +217,57 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, CGF.EmitStmt(S.getTryBody()); // Leave the try. - if (S.getNumCatchStmts()) - CGF.popCatchScope(); + llvm::BasicBlock *DispatchBlock = nullptr; + if (S.getNumCatchStmts()) { + DispatchBlock = CGF.popCatchScope(); + } + + // TODO(hugo): Better documentation + // Wasm uses Windows-style EH instructions, but merges all catch clauses into + // one big catchpad. So we save the old funclet pad here before we traverse + // each catch handler. + SaveAndRestore RestoreCurrentFuncletPad(CGF.CurrentFuncletPad); + llvm::BasicBlock *WasmCatchStartBlock = nullptr; + llvm::CatchPadInst *CPI = nullptr; + if (!!DispatchBlock && IsWasm) { + auto *CatchSwitch = + cast<llvm::CatchSwitchInst>(DispatchBlock->getFirstNonPHIIt()); + WasmCatchStartBlock = CatchSwitch->hasUnwindDest() + ? CatchSwitch->getSuccessor(1) + : CatchSwitch->getSuccessor(0); + CPI = + cast<llvm::CatchPadInst>(WasmCatchStartBlock->getFirstNonPHIIt()); + CGF.CurrentFuncletPad = CPI; + } // Remember where we were. CGBuilderTy::InsertPoint SavedIP = CGF.Builder.saveAndClearIP(); // Emit the handlers. + // TODO(hugo): Document + bool HasCatchAll = false; for (CatchHandler &Handler : Handlers) { + HasCatchAll |= Handler.TypeInfo == nullptr; CGF.EmitBlock(Handler.Block); CodeGenFunction::LexicalScope Cleanups(CGF, Handler.Body->getSourceRange()); SaveAndRestore RevertAfterScope(CGF.CurrentFuncletPad); - if (useFunclets) { + if (IsMSVC) { llvm::BasicBlock::iterator CPICandidate = Handler.Block->getFirstNonPHIIt(); if (CPICandidate != Handler.Block->end()) { - if (auto *CPI = dyn_cast_or_null<llvm::CatchPadInst>(CPICandidate)) { + CPI = dyn_cast_or_null<llvm::CatchPadInst>(CPICandidate); + if (!!CPI) { CGF.CurrentFuncletPad = CPI; CPI->setOperand(2, CGF.getExceptionSlot().emitRawPointer(CGF)); - CGF.EHStack.pushCleanup<CatchRetScope>(NormalCleanup, CPI); } } } + if (!!CPI) { + CGF.EHStack.pushCleanup<CatchRetScope>(NormalCleanup, CPI); + } + llvm::Value *RawExn = CGF.getExceptionFromSlot(); // Enter the catch. @@ -272,6 +303,10 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, CGF.EmitBranchThroughCleanup(Cont); } + if (IsWasm && !HasCatchAll) { + CGF.WasmEmitFallthroughRethrow(WasmCatchStartBlock); + } + // Go back to the try-statement fallthrough. CGF.Builder.restoreIP(SavedIP); >From afee02ea7d9ef2172a0409313e94c71eb32fb79c Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Mon, 26 Jan 2026 10:49:51 +0000 Subject: [PATCH 09/12] [ObjC][GNU] Use Cxx Exceptions on Wasm --- clang/lib/CodeGen/CGObjCGNU.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clang/lib/CodeGen/CGObjCGNU.cpp b/clang/lib/CodeGen/CGObjCGNU.cpp index f9835703a68a9..6a3ceb7d10e3e 100644 --- a/clang/lib/CodeGen/CGObjCGNU.cpp +++ b/clang/lib/CodeGen/CGObjCGNU.cpp @@ -2371,8 +2371,10 @@ CGObjCGNU::CGObjCGNU(CodeGenModule &cgm, unsigned runtimeABIVersion, auto Triple = cgm.getContext().getTargetInfo().getTriple(); msgSendMDKind = VMContext.getMDKindID("GNUObjCMessageSend"); + auto Triple = cgm.getContext().getTargetInfo().getTriple(); usesSEHExceptions = Triple.isWindowsMSVCEnvironment(); - usesCxxExceptions = (Triple.isOSCygMing() && isRuntime(ObjCRuntime::GNUstep, 2)) || Triple.isWasm(); + usesCxxExceptions = (Triple.isOSCygMing() && + isRuntime(ObjCRuntime::GNUstep, 2)) || Triple.isWasm(); CodeGenTypes &Types = CGM.getTypes(); IntTy = cast<llvm::IntegerType>( >From 8b556cc9e6e14b4edc2371c5ad0771b3cff9ba04 Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Fri, 27 Feb 2026 11:35:51 +0000 Subject: [PATCH 10/12] [Clang][ObjC] Document EH codegen --- clang/lib/CodeGen/CGObjCRuntime.cpp | 53 +++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index e33ad45fa061d..a8d9d0750acf0 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -156,6 +156,9 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, CodeGenFunction::FinallyInfo FinallyInfo; if (!useFunclets) if (const ObjCAtFinallyStmt *Finally = S.getFinallyStmt()) + // 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); @@ -186,6 +189,7 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, Handler.TypeInfo = GetEHType(CatchDecl->getType()); } + // Create a new catch scope EHCatchScope *Catch = CGF.EHStack.pushCatch(Handlers.size()); for (unsigned I = 0, E = Handlers.size(); I != E; ++I) Catch->setHandler(I, { Handlers[I].TypeInfo, Handlers[I].Flags }, Handlers[I].Block); @@ -216,16 +220,48 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, // Emit the try body. CGF.EmitStmt(S.getTryBody()); + // lpad or catch.dispatch (the dispatch block) has now been emitted + // + // Here an example: + // void may_throw(); + // @try { + // may_throw(); + // } @catch(id a) { + // } @catch(id b) { + // [...] + // + // With funclet-based exception handling, the dispatch block is created in + // getEHDispatchBlock() <- getInvokeDestImpl() <- EmitCall(). + // The following IR is emitted in this case: + // On aarch64-linux-gnu (landing-pad based) + // %call = invoke i32 @may_throw() + // to label %invoke.cont unwind label %lpad, !dbg !19 + // On aarch64-pc-windows-msvc (funclet based) + // %call = invoke i32 @may_throw() + // to label %invoke.cont unwind label %catch.dispatch, !dbg !17 + // Leave the try. llvm::BasicBlock *DispatchBlock = nullptr; if (S.getNumCatchStmts()) { + // The dispatch block that was created during the emission of the try block + // was cached. We retrieve it when popping the current catch scope. DispatchBlock = CGF.popCatchScope(); } - // TODO(hugo): Better documentation - // Wasm uses Windows-style EH instructions, but merges all catch clauses into - // one big catchpad. So we save the old funclet pad here before we traverse - // each catch handler. + // On Windows and WASM, the new exception handling instructions are used. + // + // Continuing with the previous example, on Windows, we emit one catchpad for + // every catch handler. This is not the case for WASM where all catch handlers + // merged into one big catchpad: + // + // catch.dispatch: + // %0 = catchswitch within none [label %catch.start] unwind to caller + // catch.start: + // %1 = catchpad within %0 [ptr @__objc_id_type_info, ptr null] + // [...] + // br i1 %matches, label %catch, label %catch2 + // + // We save the old funclet pad here before we traverse each catch handler. SaveAndRestore RestoreCurrentFuncletPad(CGF.CurrentFuncletPad); llvm::BasicBlock *WasmCatchStartBlock = nullptr; llvm::CatchPadInst *CPI = nullptr; @@ -243,8 +279,9 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, // Remember where we were. CGBuilderTy::InsertPoint SavedIP = CGF.Builder.saveAndClearIP(); - // Emit the handlers. - // TODO(hugo): Document + // Emit the handlers. If there is no catch-all handler, we need to emit a + // fallthrough block in WASM. We therefore need to know if we have a + // catch-all handler in this catch scope. bool HasCatchAll = false; for (CatchHandler &Handler : Handlers) { HasCatchAll |= Handler.TypeInfo == nullptr; @@ -265,6 +302,8 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, } if (!!CPI) { + // A catchpad requires a matching catchret instruction. We emit this in + // form of a cleanup. CGF.EHStack.pushCleanup<CatchRetScope>(NormalCleanup, CPI); } @@ -293,6 +332,8 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, EmitInitOfCatchParam(CGF, CastExn, CatchParam); } + // The body of the handler might have more try-catch blocks, so we need to + // save the current exception before emitting the body. CGF.ObjCEHValueStack.push_back(Exn); CGF.EmitStmt(Handler.Body); CGF.ObjCEHValueStack.pop_back(); >From 58928368c444993a654ff3ab93c093f3d247290e Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Fri, 27 Feb 2026 12:00:49 +0000 Subject: [PATCH 11/12] [CodeGen][ObjC] Disable @finally for WASM --- clang/lib/CodeGen/CGObjCRuntime.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index a8d9d0750acf0..76d7a80651604 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -25,6 +25,7 @@ #include "clang/CodeGen/CodeGenABITypes.h" #include "llvm/IR/Instruction.h" #include "llvm/IR/Instructions.h" +#include "llvm/Support/ErrorHandling.h" #include "llvm/Support/SaveAndRestore.h" using namespace clang; @@ -154,13 +155,17 @@ void CGObjCRuntime::EmitTryCatchStmt(CodeGenFunction &CGF, bool IsMSVC = EHPersonality::get(CGF).isMSVCPersonality(); CodeGenFunction::FinallyInfo FinallyInfo; - if (!useFunclets) - if (const ObjCAtFinallyStmt *Finally = S.getFinallyStmt()) + if (const ObjCAtFinallyStmt *Finally = S.getFinallyStmt()) { + if (!useFunclets) { // 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); + FinallyInfo.enter(CGF, Finally->getFinallyBody(), beginCatchFn, + endCatchFn, exceptionRethrowFn); + } else if (IsWasm) { + llvm_unreachable("@finally not implemented for WASM"); + } + } SmallVector<CatchHandler, 8> Handlers; >From f152b462b7d84823cf1b6a0a2164d1418ead6fa1 Mon Sep 17 00:00:00 2001 From: hmelder <[email protected]> Date: Fri, 27 Feb 2026 13:57:28 +0000 Subject: [PATCH 12/12] [CodeGen][ObjC] Add WASM EH test case --- clang/test/CodeGenObjC/gnustep2-wasm32-eh.m | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 clang/test/CodeGenObjC/gnustep2-wasm32-eh.m diff --git a/clang/test/CodeGenObjC/gnustep2-wasm32-eh.m b/clang/test/CodeGenObjC/gnustep2-wasm32-eh.m new file mode 100644 index 0000000000000..08aadfe507003 --- /dev/null +++ b/clang/test/CodeGenObjC/gnustep2-wasm32-eh.m @@ -0,0 +1,37 @@ +// RUN: %clang_cc1 -triple wasm32-unknown-emscripten -fobjc-exceptions -fexceptions -exception-model=wasm -mllvm -wasm-enable-eh -emit-llvm -fobjc-runtime=gnustep-2.2 -o - %s | FileCheck %s + +void may_throw(void) { + @throw (id) 1; +} + +int main(void) { + int retval = 0; + @try { + may_throw(); + // CHECK: invoke void @may_throw() + // CHECK-NEXT: to label %[[INVOKE_CONT:.*]] unwind label %[[CATCH_DISPATCH:.*]] + } + // Check that the dispatch block has been emitted correctly. + // CHECK: [[CATCH_DISPATCH]]: + // CHECK-NEXT: %[[CATCHSWITCH:.*]] = catchswitch within none [label %[[CATCH_START:.*]] unwind to caller + + + // The native WASM EH uses the new exception handling IR instructions + // (catchswitch, catchpad, etc.) that are also used when targeting Windows MSVC. + // For SEH, we emit a catchpad instruction for each catch statement. On WASM, we + // merge all catch statements into one big catch block. + + // CHECK: catchpad within %[[CATCHSWITCH]] [ptr @__objc_id_type_info, ptr null] + + // We use the cxa functions instead of objc_{begin,end}_catch. + // CHECK: call ptr @__cxa_begin_catch + @catch(id a) { + retval = 1; + } + @catch(...) { + retval = 2; + } + return retval; +} + + _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
