diff --git a/llvm/include/llvm/Transforms/Utils/Local.h b/llvm/include/llvm/Transforms/Utils/Local.h --- a/llvm/include/llvm/Transforms/Utils/Local.h +++ b/llvm/include/llvm/Transforms/Utils/Local.h @@ -241,7 +241,7 @@ CallInst *createCallMatchingInvoke(InvokeInst *II); /// This function converts the specified invoek into a normall call. -void changeToCall(InvokeInst *II, DomTreeUpdater *DTU = nullptr); +CallInst *changeToCall(InvokeInst *II, DomTreeUpdater *DTU = nullptr); ///===---------------------------------------------------------------------===// /// Dbg Intrinsic utilities diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp b/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp --- a/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp +++ b/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp @@ -611,6 +611,8 @@ return false; StringRef CalleeName = Callee->getName(); + // TODO Include more functions or consider checking with mangled prefixes + // The reason we include malloc/free here is to exclude the malloc/free // calls generated in setjmp prep / cleanup routines. if (CalleeName == "setjmp" || CalleeName == "malloc" || CalleeName == "free") @@ -627,11 +629,50 @@ return false; // Exception-catching related functions - if (CalleeName == "__cxa_begin_catch" || CalleeName == "__cxa_end_catch" || + // + // We intentionally excluded __cxa_end_catch here even though it surely cannot + // longjmp, in order to maintain the unwind relationship from all existing + // catchpads (and calls within them) to catch.dispatch.longjmp. + // + // In Wasm EH + Wasm SjLj, we + // 1. Make all catchswitch and cleanuppad that unwind to caller unwind to + // catch.dispatch.longjmp instead + // 2. Convert all longjmpable calls to invokes that unwind to + // catch.dispatch.longjmp + // But catchswitch BBs are removed in isel, so if an EH catchswitch (generated + // from an exception)'s catchpad does not contain any calls that are converted + // into invokes unwinding to catch.dispatch.longjmp, this unwind relationship + // (EH catchswitch BB -> catch.dispatch.longjmp BB) is lost and + // catch.dispatch.longjmp BB can be placed before the EH catchswitch BB in + // CFGSort. + // int ret = setjmp(buf); + // try { + // foo(); // longjmps + // } catch (...) { + // } + // Then in this code, if 'foo' longjmps, it first unwinds to 'catch (...)' + // catchswitch, and is not caught by that catchswitch because it is a longjmp, + // then it should next unwind to catch.dispatch.longjmp BB. But if this 'catch + // (...)' catchswitch -> catch.dispatch.longjmp unwind relationship is lost, + // it will not unwind to catch.dispatch.longjmp, producing an incorrect + // result. + // + // Every catchpad generated by Wasm C++ contains __cxa_end_catch, so we + // intentionally treat it as longjmpable to work around this problem. This is + // a hacky fix but an easy one. + // + // The comment block in findWasmUnwindDestinations() in + // SelectionDAGBuilder.cpp is addressing a similar problem. + if (CalleeName == "__cxa_begin_catch" || CalleeName == "__cxa_allocate_exception" || CalleeName == "__cxa_throw" || CalleeName == "__clang_call_terminate") return false; + // std::terminate, which is generated when another exception occurs while + // handling an exception, cannot longjmp. + if (CalleeName == "_ZSt9terminatev") + return false; + // Otherwise we don't know return true; } @@ -1271,16 +1312,19 @@ // Setjmp transformation SmallVector SetjmpRetPHIs; Function *SetjmpF = M.getFunction("setjmp"); - for (User *U : SetjmpF->users()) { - auto *CI = dyn_cast(U); - // FIXME 'invoke' to setjmp can happen when we use Wasm EH + Wasm SjLj, but - // we don't support two being used together yet. - if (!CI) - report_fatal_error("Wasm EH + Wasm SjLj is not fully supported yet"); - BasicBlock *BB = CI->getParent(); + for (auto *U : make_early_inc_range(SetjmpF->users())) { + auto *CB = dyn_cast(U); + BasicBlock *BB = CB->getParent(); if (BB->getParent() != &F) // in other function continue; + CallInst *CI = nullptr; + // setjmp cannot throw. So if it is an invoke, lower it to a call + if (auto *II = dyn_cast(CB)) + CI = llvm::changeToCall(II); + else + CI = cast(CB); + // The tail is everything right after the call, and will be reached once // when setjmp is called, and later when longjmp returns to the setjmp BasicBlock *Tail = SplitBlock(BB, CI->getNextNode()); @@ -1596,6 +1640,13 @@ I->eraseFromParent(); } +static BasicBlock *getCleanupRetUnwindDest(const CleanupPadInst *CPI) { + for (const User *U : CPI->users()) + if (const auto *CRI = dyn_cast(U)) + return CRI->getUnwindDest(); + return nullptr; +} + // Create a catchpad in which we catch a longjmp's env and val arguments, test // if the longjmp corresponds to one of setjmps in the current function, and if // so, jump to the setjmp dispatch BB from which we go to one of post-setjmp @@ -1747,6 +1798,7 @@ LongjmpableCalls.push_back(CI); } } + for (auto *CI : LongjmpableCalls) { // Even if the callee function has attribute 'nounwind', which is true for // all C functions, it can longjmp, which means it can throw a Wasm @@ -1754,8 +1806,58 @@ CI->removeFnAttr(Attribute::NoUnwind); if (Function *CalleeF = CI->getCalledFunction()) CalleeF->removeFnAttr(Attribute::NoUnwind); + // Change it to an invoke and make it unwind to the catch.dispatch.longjmp - // BB. - changeToInvokeAndSplitBasicBlock(CI, CatchDispatchLongjmpBB); + // BB. If the call is enclosed in another catchpad/cleanuppad scope, unwind + // to its parent pad's unwind destination instead to preserve the scope + // structure. It will eventually unwind to the catch.dispatch.longjmp. + SmallVector Bundles; + BasicBlock *UnwindDest = nullptr; + if (auto Bundle = CI->getOperandBundle(LLVMContext::OB_funclet)) { + Instruction *FromPad = cast(Bundle->Inputs[0]); + while (!UnwindDest && FromPad) { + if (auto *CPI = dyn_cast(FromPad)) { + UnwindDest = CPI->getCatchSwitch()->getUnwindDest(); + FromPad = nullptr; // stop searching + } else if (auto *CPI = dyn_cast(FromPad)) { + // getCleanupRetUnwindDest() can return nullptr when + // 1. This cleanuppad's matching cleanupret uwninds to caller + // 2. There is no matching cleanupret because it ends with + // unreachable. + // In case of 2, we need to traverse the parent pad chain. + UnwindDest = getCleanupRetUnwindDest(CPI); + FromPad = cast(CPI->getParentPad()); + } + } + } + if (!UnwindDest) + UnwindDest = CatchDispatchLongjmpBB; + changeToInvokeAndSplitBasicBlock(CI, UnwindDest); + } + + SmallVector ToErase; + for (auto &BB : F) { + if (auto *CSI = dyn_cast(BB.getFirstNonPHI())) { + if (CSI != CatchSwitchLongjmp && CSI->unwindsToCaller()) { + IRB.SetInsertPoint(CSI); + ToErase.push_back(CSI); + auto *NewCSI = IRB.CreateCatchSwitch(CSI->getParentPad(), + CatchDispatchLongjmpBB, 1); + NewCSI->addHandler(*CSI->handler_begin()); + NewCSI->takeName(CSI); + CSI->replaceAllUsesWith(NewCSI); + } + } + + if (auto *CRI = dyn_cast(BB.getTerminator())) { + if (CRI->unwindsToCaller()) { + IRB.SetInsertPoint(CRI); + ToErase.push_back(CRI); + IRB.CreateCleanupRet(CRI->getCleanupPad(), CatchDispatchLongjmpBB); + } + } } + + for (Instruction *I : ToErase) + I->eraseFromParent(); } diff --git a/llvm/lib/Transforms/Utils/Local.cpp b/llvm/lib/Transforms/Utils/Local.cpp --- a/llvm/lib/Transforms/Utils/Local.cpp +++ b/llvm/lib/Transforms/Utils/Local.cpp @@ -2189,8 +2189,8 @@ return NewCall; } -/// changeToCall - Convert the specified invoke into a normal call. -void llvm::changeToCall(InvokeInst *II, DomTreeUpdater *DTU) { +// changeToCall - Convert the specified invoke into a normal call. +CallInst *llvm::changeToCall(InvokeInst *II, DomTreeUpdater *DTU) { CallInst *NewCall = createCallMatchingInvoke(II); NewCall->takeName(II); NewCall->insertBefore(II); @@ -2207,6 +2207,7 @@ II->eraseFromParent(); if (DTU) DTU->applyUpdates({{DominatorTree::Delete, BB, UnwindDestBB}}); + return NewCall; } BasicBlock *llvm::changeToInvokeAndSplitBasicBlock(CallInst *CI, diff --git a/llvm/test/CodeGen/WebAssembly/lower-wasm-ehsjlj.ll b/llvm/test/CodeGen/WebAssembly/lower-wasm-ehsjlj.ll new file mode 100644 --- /dev/null +++ b/llvm/test/CodeGen/WebAssembly/lower-wasm-ehsjlj.ll @@ -0,0 +1,259 @@ +; RUN: opt < %s -wasm-lower-em-ehsjlj -wasm-enable-eh -wasm-enable-sjlj -S | FileCheck %s +; RUN: llc < %s -wasm-enable-eh -wasm-enable-sjlj -exception-model=wasm -mattr=+exception-handling -verify-machineinstrs + +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32-unknown-unknown" + +%struct.__jmp_buf_tag = type { [6 x i32], i32, [32 x i32] } +@_ZL3buf = internal global [1 x %struct.__jmp_buf_tag] zeroinitializer, align 16 + +; void test() { +; int jmpval = setjmp(buf); +; if (jmpval != 0) +; return; +; try { +; foo(); +; } catch (...) { +; } +; } +define void @setjmp_and_try() personality i8* bitcast (i32 (...)* @__gxx_wasm_personality_v0 to i8*) { +; CHECK-LABEL: @setjmp_and_try( + +entry: + %call = call i32 @setjmp(%struct.__jmp_buf_tag* getelementptr inbounds ([1 x %struct.__jmp_buf_tag], [1 x %struct.__jmp_buf_tag]* @_ZL3buf, i32 0, i32 0)) #0 + %cmp = icmp ne i32 %call, 0 + br i1 %cmp, label %return, label %if.end + +if.end: ; preds = %entry + invoke void @foo() + to label %return unwind label %catch.dispatch + +catch.dispatch: ; preds = %if.end + %0 = catchswitch within none [label %catch.start] unwind to caller +; CHECK: catch.dispatch: +; CHECK-NEXT: catchswitch within none [label %catch.start] unwind label %catch.dispatch.longjmp + +catch.start: ; preds = %catch.dispatch + %1 = catchpad within %0 [i8* null] + %2 = call i8* @llvm.wasm.get.exception(token %1) + %3 = call i32 @llvm.wasm.get.ehselector(token %1) + %4 = call i8* @__cxa_begin_catch(i8* %2) #2 [ "funclet"(token %1) ] + call void @__cxa_end_catch() [ "funclet"(token %1) ] + catchret from %1 to label %return +; CHECK: catch.start: +; CHECK: [[T0:%.*]] = catchpad within {{.*}} [i8* null] +; CHECK: invoke void @__cxa_end_catch() [ "funclet"(token [[T0]]) ] +; CHECK-NEXT: to label %.noexc unwind label %catch.dispatch.longjmp + +; CHECK: .noexc: +; CHECK-NEXT: catchret from [[T0]] to label {{.*}} + +return: ; preds = %catch.start, %if.end, %entry + ret void + +; CHECK: catch.dispatch.longjmp: +; CHECK-NEXT: catchswitch within none [label %catch.longjmp] unwind to caller +} + +; void setjmp_within_try() { +; try { +; foo(); +; int jmpval = setjmp(buf); +; if (jmpval != 0) +; return; +; foo(); +; } catch (...) { +; } +; } +define void @setjmp_within_try() personality i8* bitcast (i32 (...)* @__gxx_wasm_personality_v0 to i8*) { +; CHECK-LABEL: @setjmp_within_try( +entry: + %jmpval = alloca i32, align 4 + %exn.slot = alloca i8*, align 4 + invoke void @foo() + to label %invoke.cont unwind label %catch.dispatch + +invoke.cont: ; preds = %entry + %call = invoke i32 @setjmp(%struct.__jmp_buf_tag* getelementptr inbounds ([1 x %struct.__jmp_buf_tag], [1 x %struct.__jmp_buf_tag]* @_ZL3buf, i32 0, i32 0)) #0 + to label %invoke.cont1 unwind label %catch.dispatch + +invoke.cont1: ; preds = %invoke.cont + store i32 %call, i32* %jmpval, align 4 + %0 = load i32, i32* %jmpval, align 4 + %cmp = icmp ne i32 %0, 0 + br i1 %cmp, label %if.then, label %if.end + +if.then: ; preds = %invoke.cont1 + br label %try.cont + +if.end: ; preds = %invoke.cont1 + invoke void @foo() + to label %invoke.cont2 unwind label %catch.dispatch + +catch.dispatch: ; preds = %if.end, %invoke.cont, %entry + %1 = catchswitch within none [label %catch.start] unwind to caller + +; CHECK: catch.dispatch: +; CHECK: catchswitch within none [label %catch.start] unwind label %catch.dispatch.longjmp +catch.start: ; preds = %catch.dispatch + %2 = catchpad within %1 [i8* null] + %3 = call i8* @llvm.wasm.get.exception(token %2) + store i8* %3, i8** %exn.slot, align 4 + %4 = call i32 @llvm.wasm.get.ehselector(token %2) + br label %catch + +catch: ; preds = %catch.start + %exn = load i8*, i8** %exn.slot, align 4 + %5 = call i8* @__cxa_begin_catch(i8* %exn) #2 [ "funclet"(token %2) ] + call void @__cxa_end_catch() [ "funclet"(token %2) ] + catchret from %2 to label %catchret.dest +; CHECK: catch: ; preds = %catch.start +; CHECK-NEXT: %exn = load i8*, i8** %exn.slot15, align 4 +; CHECK-NEXT: %5 = call i8* @__cxa_begin_catch(i8* %exn) #2 [ "funclet"(token %2) ] +; CHECK-NEXT: invoke void @__cxa_end_catch() [ "funclet"(token %2) ] +; CHECK-NEXT: to label %.noexc unwind label %catch.dispatch.longjmp + +catchret.dest: ; preds = %catch + br label %try.cont + +try.cont: ; preds = %invoke.cont2, %catchret.dest, %if.then + ret void + +invoke.cont2: ; preds = %if.end + br label %try.cont + +; CHECK: catch.dispatch.longjmp: +; CHECK-NEXT: catchswitch within none [label %catch.longjmp] unwind to caller +} + +; void setjmp_and_nested_try() { +; int jmpval = setjmp(buf); +; if (jmpval != 0) +; return; +; try { +; foo(); +; try { +; foo(); +; } catch (...) { +; foo(); +; } +; } catch (...) { +; } +; } +define void @setjmp_and_nested_try() personality i8* bitcast (i32 (...)* @__gxx_wasm_personality_v0 to i8*) { +; CHECK-LABEL: @setjmp_and_nested_try( +entry: + %call = call i32 @setjmp(%struct.__jmp_buf_tag* getelementptr inbounds ([1 x %struct.__jmp_buf_tag], [1 x %struct.__jmp_buf_tag]* @_ZL3buf, i32 0, i32 0)) #0 + %cmp = icmp ne i32 %call, 0 + br i1 %cmp, label %try.cont10, label %if.end + +if.end: ; preds = %entry + invoke void @foo() + to label %invoke.cont unwind label %catch.dispatch5 + +invoke.cont: ; preds = %if.end + invoke void @foo() + to label %try.cont10 unwind label %catch.dispatch + +catch.dispatch: ; preds = %invoke.cont + %0 = catchswitch within none [label %catch.start] unwind label %catch.dispatch5 + +catch.start: ; preds = %catch.dispatch + %1 = catchpad within %0 [i8* null] + %2 = call i8* @llvm.wasm.get.exception(token %1) + %3 = call i32 @llvm.wasm.get.ehselector(token %1) + %4 = call i8* @__cxa_begin_catch(i8* %2) #2 [ "funclet"(token %1) ] + invoke void @foo() [ "funclet"(token %1) ] + to label %invoke.cont2 unwind label %ehcleanup + +invoke.cont2: ; preds = %catch.start + invoke void @__cxa_end_catch() [ "funclet"(token %1) ] + to label %invoke.cont3 unwind label %catch.dispatch5 + +invoke.cont3: ; preds = %invoke.cont2 + catchret from %1 to label %try.cont10 + +ehcleanup: ; preds = %catch.start + %5 = cleanuppad within %1 [] + invoke void @__cxa_end_catch() [ "funclet"(token %5) ] + to label %invoke.cont4 unwind label %terminate +; CHECK: ehcleanup: +; CHECK-NEXT: [[T0:%.*]] = cleanuppad within {{.*}} [] +; CHECK-NEXT: invoke void @__cxa_end_catch() [ "funclet"(token [[T0]]) ] +; CHECK-NEXT: to label %invoke.cont4 unwind label %terminate + +invoke.cont4: ; preds = %ehcleanup + cleanupret from %5 unwind label %catch.dispatch5 +; CHECK: invoke.cont4: +; CHECK-NEXT: cleanupret from [[T0]] unwind label %catch.dispatch5 + +catch.dispatch5: ; preds = %invoke.cont4, %invoke.cont2, %catch.dispatch, %if.end + %6 = catchswitch within none [label %catch.start6] unwind to caller +; CHECK: catch.dispatch5: +; CHECK-NEXT: catchswitch within none [label %catch.start6] unwind label %catch.dispatch.longjmp + +catch.start6: ; preds = %catch.dispatch5 + %7 = catchpad within %6 [i8* null] + %8 = call i8* @llvm.wasm.get.exception(token %7) + %9 = call i32 @llvm.wasm.get.ehselector(token %7) + %10 = call i8* @__cxa_begin_catch(i8* %8) #2 [ "funclet"(token %7) ] + call void @__cxa_end_catch() [ "funclet"(token %7) ] + catchret from %7 to label %try.cont10 +; CHECK: catch.start6: +; CHECK-NEXT: [[T1:%.*]] = catchpad within {{.*}} [i8* null] +; CHECK-NEXT: call i8* @llvm.wasm.get.exception(token [[T1]]) +; CHECK-NEXT: call i32 @llvm.wasm.get.ehselector(token [[T1]]) +; CHECK-NEXT: call i8* @__cxa_begin_catch(i8* {{.*}}) {{.*}} [ "funclet"(token [[T1]]) ] +; CHECK: invoke void @__cxa_end_catch() [ "funclet"(token [[T1]]) ] +; CHECK-NEXT: to label %.noexc unwind label %catch.dispatch.longjmp + +; CHECK: .noexc: +; CHECK-NEXT: catchret from [[T1]] to label {{.*}} + +try.cont10: ; preds = %catch.start6, %invoke.cont3, %invoke.cont, %entry + ret void + +terminate: ; preds = %ehcleanup + %11 = cleanuppad within %5 [] + call void @terminate() #3 [ "funclet"(token %11) ] + unreachable +; CHECK: terminate: +; CHECK-NEXT: [[T2:%.*]] = cleanuppad within [[T0]] [] +; Note that this call unwinds not to %catch.dispatch.longjmp but to +; %catch.dispatch5. This call is enclosed in the cleanuppad above, but there is +; no matching catchret, which has the unwind destination. So this checks this +; cleanuppad's parent, which is in 'ehcleanup', and unwinds to its unwind +; destination, %catch.dispatch5. +; This call was originally '_ZSt9terminatev', which is the mangled name for +; 'std::terminate'. But we listed that as "cannot longjmp", we changed +; the name of the function in this test to show the case in which a call has to +; change to an invoke whose unwind destination is determined by its parent +; chain. +; CHECK-NEXT: invoke void @terminate() {{.*}} [ "funclet"(token [[T2]]) ] +; CHECK-NEXT: to label %.noexc4 unwind label %catch.dispatch5 + +; CHECK: .noexc4: +; CHECK-NEXT: unreachable + +; CHECK: catch.dispatch.longjmp: +; CHECK-NEXT: catchswitch within none [label %catch.longjmp] unwind to caller +} + +declare void @foo() +; Function Attrs: returns_twice +declare i32 @setjmp(%struct.__jmp_buf_tag*) #0 +; Function Attrs: noreturn +declare void @longjmp(%struct.__jmp_buf_tag*, i32) #1 +declare i32 @__gxx_wasm_personality_v0(...) +; Function Attrs: nounwind +declare i8* @llvm.wasm.get.exception(token) #2 +; Function Attrs: nounwind +declare i32 @llvm.wasm.get.ehselector(token) #2 +declare i8* @__cxa_begin_catch(i8*) +declare void @__cxa_end_catch() +declare void @terminate() + +attributes #0 = { returns_twice } +attributes #1 = { noreturn } +attributes #2 = { nounwind } +attributes #3 = { noreturn nounwind }