Skip to content

Commit 61431d5

Browse files
committedAug 4, 2024
wasm: add recover support
Add unwinding and recover support for wasm using WebAssembly exception handling. This still has a few gotchas: * Many WASI systems don't support exception handling yet. For example, see: bytecodealliance/wasmtime#2049 * Asyncify doesn't support wasm exception handling: WebAssembly/binaryen#4470 This means it's not possible to use goroutines together with panic/recover. * The current way that exceptions are implemented pretend to be C++ exceptions, but work slightly differently. If C++ code is called (for example through CGo) that raises an exception, that exception will be eaten by TinyGo and not be propagated. This is fixable, it just hasn't been implemented (because we don't actually support C++ right now). I hope that these issues will be resolved over time. At least for now, people who need `recover()` have a way to use it.
1 parent 39fa7a1 commit 61431d5

28 files changed

+886
-398
lines changed
 

‎builder/build.go

+1
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
745745
ldflags = append(ldflags, dependency.result)
746746
}
747747
ldflags = append(ldflags, "-mllvm", "-mcpu="+config.CPU())
748+
ldflags = append(ldflags, "-mllvm", "-wasm-enable-eh")
748749
if config.GOOS() == "windows" {
749750
// Options for the MinGW wrapper for the lld COFF linker.
750751
ldflags = append(ldflags,

‎compiler/calls.go

+23-8
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,25 @@ func (b *builder) createRuntimeInvoke(fnName string, args []llvm.Value, name str
7070
// createCall creates a call to the given function with the arguments possibly
7171
// expanded.
7272
func (b *builder) createCall(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string) llvm.Value {
73-
expanded := make([]llvm.Value, 0, len(args))
74-
for _, arg := range args {
75-
fragments := b.expandFormalParam(arg)
76-
expanded = append(expanded, fragments...)
77-
}
78-
return b.CreateCall(fnType, fn, expanded, name)
73+
return b.CreateCall(fnType, fn, b.expandFormalParams(args), name)
7974
}
8075

8176
// createInvoke is like createCall but continues execution at the landing pad if
8277
// the call resulted in a panic.
8378
func (b *builder) createInvoke(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string) llvm.Value {
84-
if b.hasDeferFrame() {
79+
switch b.deferFrameType() {
80+
case recoverInlineAsm:
8581
b.createInvokeCheckpoint()
82+
return b.createCall(fnType, fn, args, name)
83+
case recoverWasmEH:
84+
continueBB := b.insertBasicBlock("invoke.cont")
85+
call := b.CreateInvoke(fnType, fn, b.expandFormalParams(args), continueBB, b.landingpad, name)
86+
b.SetInsertPointAtEnd(continueBB)
87+
b.blockExits[b.currentBlock] = continueBB
88+
return call
89+
default:
90+
return b.createCall(fnType, fn, args, name)
8691
}
87-
return b.createCall(fnType, fn, args, name)
8892
}
8993

9094
// Expand an argument type to a list that can be used in a function call
@@ -123,6 +127,17 @@ func (b *builder) expandFormalParamOffsets(t llvm.Type) []uint64 {
123127
}
124128
}
125129

130+
// expandFormalParams expands every param in the params slice like
131+
// expandFormalParam.
132+
func (b *builder) expandFormalParams(params []llvm.Value) []llvm.Value {
133+
expanded := make([]llvm.Value, 0, len(params))
134+
for _, arg := range params {
135+
fragments := b.expandFormalParam(arg)
136+
expanded = append(expanded, fragments...)
137+
}
138+
return expanded
139+
}
140+
126141
// expandFormalParam splits a formal param value into pieces, so it can be
127142
// passed directly as part of a function call. For example, it splits up small
128143
// structs into individual fields. It is the equivalent of expandFormalParamType

‎compiler/compiler.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,10 @@ func CompilePackage(moduleName string, pkg *loader.Package, ssaPkg *ssa.Package,
355355
}
356356

357357
func (c *compilerContext) getRuntimeType(name string) types.Type {
358-
return c.runtimePkg.Scope().Lookup(name).(*types.TypeName).Type()
358+
if typ, ok := c.runtimePkg.Scope().Lookup(name).(*types.TypeName); ok {
359+
return typ.Type()
360+
}
361+
panic("runtime type not found: " + name)
359362
}
360363

361364
// getLLVMRuntimeType obtains a named type from the runtime package and returns
@@ -1350,7 +1353,7 @@ func (b *builder) createFunction() {
13501353
b.CreateBr(b.afterDefersBlock[i])
13511354
}
13521355

1353-
if b.hasDeferFrame() {
1356+
if b.deferFrameType() != recoverNone {
13541357
// Create the landing pad block, where execution continues after a
13551358
// panic.
13561359
b.createLandingPad()
@@ -1487,8 +1490,11 @@ func (b *builder) createInstruction(instr ssa.Instruction) {
14871490
b.createRuntimeInvoke("_panic", []llvm.Value{value}, "")
14881491
b.CreateUnreachable()
14891492
case *ssa.Return:
1490-
if b.hasDeferFrame() {
1491-
b.createRuntimeCall("destroyDeferFrame", []llvm.Value{b.deferFrame}, "")
1493+
switch b.deferFrameType() {
1494+
case recoverInlineAsm:
1495+
b.createRuntimeCall("destroyDeferFrameInlineAsm", []llvm.Value{b.deferFrame}, "")
1496+
case recoverWasmEH:
1497+
b.createRuntimeCall("destroyDeferFrameWasmEH", []llvm.Value{b.deferFrame}, "")
14921498
}
14931499
if len(instr.Results) == 0 {
14941500
b.CreateRetVoid()
@@ -1741,7 +1747,7 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c
17411747
return b.CreateExtractValue(cplx, 0, "real"), nil
17421748
case "recover":
17431749
useParentFrame := uint64(0)
1744-
if b.hasDeferFrame() {
1750+
if b.fn.Recover != nil {
17451751
// recover() should return the panic value of the parent function,
17461752
// not of the current function.
17471753
useParentFrame = 1
@@ -1851,11 +1857,8 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error)
18511857
case strings.HasPrefix(name, "syscall.rawSyscallNoError") || strings.HasPrefix(name, "golang.org/x/sys/unix.RawSyscallNoError"):
18521858
return b.createRawSyscallNoError(instr)
18531859
case name == "runtime.supportsRecover":
1854-
supportsRecover := uint64(0)
1855-
if b.supportsRecover() {
1856-
supportsRecover = 1
1857-
}
1858-
return llvm.ConstInt(b.ctx.Int1Type(), supportsRecover, false), nil
1860+
supportsRecover := uint64(b.supportsRecover())
1861+
return llvm.ConstInt(b.ctx.Int8Type(), supportsRecover, false), nil
18591862
case name == "runtime.panicStrategy":
18601863
// These constants are defined in src/runtime/panic.go.
18611864
panicStrategy := map[string]uint64{

‎compiler/compiler_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func TestCompiler(t *testing.T) {
4444
{"interface.go", "", ""},
4545
{"func.go", "", ""},
4646
{"defer.go", "cortex-m-qemu", ""},
47+
{"defer.go", "wasm", "none"},
4748
{"pragma.go", "", ""},
4849
{"goroutine.go", "wasm", "asyncify"},
4950
{"goroutine.go", "cortex-m-qemu", "tasks"},

0 commit comments

Comments
 (0)
Failed to load comments.