From b8b831cf5ca7989052ca9905c0a109a3df07145b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:46:55 -0700 Subject: [PATCH] runtime: support panic recovery on Xtensa Use picolibc setjmp and longjmp to save and restore the complete Xtensa register-window state at TinyGo panic checkpoints. Provide the target configuration and window spill helper required by picolibc. Increment its cache version because Xtensa archives now include another source file. --- builder/picolibc.go | 20 ++++++++++-- compileopts/config.go | 2 +- compiler/defer.go | 21 ++++++++----- lib/xtensa/include/xtensa/config/core-isa.h | 18 +++++++++++ make/release.mk | 22 +++++++------ src/runtime/arch_xtensa.go | 4 ++- src/runtime/asm_xtensa.S | 34 +++++++++++++++++++++ targets/xtensa.json | 3 +- 8 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 lib/xtensa/include/xtensa/config/core-isa.h create mode 100644 src/runtime/asm_xtensa.S diff --git a/builder/picolibc.go b/builder/picolibc.go index f6712a5cd..5ceda5c29 100644 --- a/builder/picolibc.go +++ b/builder/picolibc.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" + "github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/goenv" ) @@ -19,8 +20,9 @@ var libPicolibc = Library{ return f.Close() }, cflags: func(target, headerPath string) []string { - picolibcDir := filepath.Join(goenv.Get("TINYGOROOT"), "lib/picolibc") - return []string{ + root := goenv.Get("TINYGOROOT") + picolibcDir := filepath.Join(root, "lib/picolibc") + flags := []string{ "-Werror", "-Wall", "-std=gnu11", @@ -39,10 +41,22 @@ var libPicolibc = Library{ "-I" + picolibcDir + "/libm/common", "-I" + headerPath, } + if compileopts.CanonicalArchName(target) == "xtensa" { + flags = append(flags, + "-I"+filepath.Join(root, "lib/xtensa/include"), + "-I"+picolibcDir+"/libc/machine/xtensa", + "-D_XTENSA_HAVE_CONFIG_CORE_ISA_H", + ) + } + return flags }, sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/picolibc") }, librarySources: func(target string, _ bool) ([]string, error) { - return append([]string(nil), picolibcSources...), nil + sources := append([]string(nil), picolibcSources...) + if compileopts.CanonicalArchName(target) == "xtensa" { + sources = append(sources, "libc/machine/xtensa/setjmp.S") + } + return sources, nil }, } diff --git a/compileopts/config.go b/compileopts/config.go index d9a8ce2c0..777cb782e 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -25,7 +25,7 @@ import ( var libVersions = map[string]int{ "musl": 3, "bdwgc": 2, - "picolibc": 1, + "picolibc": 2, "wasmbuiltins": 1, } diff --git a/compiler/defer.go b/compiler/defer.go index f8078f6b5..d263720af 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -32,9 +32,6 @@ func (b *builder) supportsRecover() bool { // proposal of WebAssembly: // https://github.com/WebAssembly/exception-handling return false - case "xtensa": - // TODO: add support for these architectures - return false default: return true } @@ -109,11 +106,21 @@ func (b *builder) createLandingPad() { b.CreateBr(b.blockInfo[b.fn.Recover.Index].entry) } -// Create a checkpoint (similar to setjmp). This emits inline assembly that -// stores the current program counter inside the ptr address (actually -// ptr+sizeof(ptr)) and then returns a boolean indicating whether this is the -// normal flow (false) or we jumped here from somewhere else (true). +// Create a checkpoint (similar to setjmp). It returns whether execution is +// continuing normally instead of resuming after a longjmp. func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value { + if b.archFamily() == "xtensa" { + fnType := llvm.FunctionType(b.ctx.Int32Type(), []llvm.Type{b.dataPtrType}, false) + fn := b.mod.NamedFunction("setjmp") + if fn.IsNil() { + fn = llvm.AddFunction(b.mod, "setjmp", fnType) + fn.AddFunctionAttr(b.ctx.CreateEnumAttribute(llvm.AttributeKindID("returns_twice"), 0)) + } + result := b.CreateCall(fnType, fn, []llvm.Value{ptr}, "setjmp") + result.AddCallSiteAttribute(-1, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("returns_twice"), 0)) + return b.CreateICmp(llvm.IntEQ, result, llvm.ConstInt(b.ctx.Int32Type(), 0, false), "setjmp.result") + } + // Construct inline assembly equivalents of setjmp. // The assembly works as follows: // * Registers are either clobbered or, on 386, saved for longjmp to diff --git a/lib/xtensa/include/xtensa/config/core-isa.h b/lib/xtensa/include/xtensa/config/core-isa.h new file mode 100644 index 000000000..ceed83ed6 --- /dev/null +++ b/lib/xtensa/include/xtensa/config/core-isa.h @@ -0,0 +1,18 @@ +#ifndef TINYGO_XTENSA_CONFIG_CORE_ISA_H +#define TINYGO_XTENSA_CONFIG_CORE_ISA_H + +#ifdef __XTENSA_WINDOWED_ABI__ +#define XCHAL_HAVE_WINDOWED 1 +#else +#define XCHAL_HAVE_WINDOWED 0 +#endif + +#ifdef __XTENSA_EB__ +#define XCHAL_HAVE_BE 1 +#else +#define XCHAL_HAVE_BE 0 +#endif + +#define XCHAL_MAYHAVE_ERRATUM_XEA1KWIN 1 + +#endif diff --git a/make/release.mk b/make/release.mk index 23724e1d9..bf2e0f09a 100644 --- a/make/release.mk +++ b/make/release.mk @@ -15,12 +15,14 @@ build/release: tinygo gen-device $(if $(filter 1,$(USE_SYSTEM_BINARYEN)),,binary @mkdir -p build/release/tinygo/lib/musl/src @mkdir -p build/release/tinygo/lib/nrfx @mkdir -p build/release/tinygo/lib/picolibc/libc + @mkdir -p build/release/tinygo/lib/picolibc/libc/machine @mkdir -p build/release/tinygo/lib/picolibc/libm @mkdir -p build/release/tinygo/lib/wasi-libc/dlmalloc @mkdir -p build/release/tinygo/lib/wasi-libc/libc-bottom-half @mkdir -p build/release/tinygo/lib/wasi-libc/libc-top-half/musl/arch @mkdir -p build/release/tinygo/lib/wasi-libc/libc-top-half/musl/src @mkdir -p build/release/tinygo/lib/wasi-cli/ + @mkdir -p build/release/tinygo/lib/xtensa @echo copying source files @cp -p build/tinygo$(EXE) build/release/tinygo/bin ifneq ($(USE_SYSTEM_BINARYEN),1) @@ -80,15 +82,16 @@ endif @cp -rp lib/mingw-w64/mingw-w64-headers/defaults/include build/release/tinygo/lib/mingw-w64/mingw-w64-headers/defaults @cp -rp lib/mingw-w64/mingw-w64-headers/include build/release/tinygo/lib/mingw-w64/mingw-w64-headers @cp -rp lib/nrfx/* build/release/tinygo/lib/nrfx - @cp -rp lib/picolibc/libc/ctype build/release/tinygo/lib/picolibc/libc - @cp -rp lib/picolibc/libc/include build/release/tinygo/lib/picolibc/libc - @cp -rp lib/picolibc/libc/locale build/release/tinygo/lib/picolibc/libc - @cp -rp lib/picolibc/libc/stdlib build/release/tinygo/lib/picolibc/libc - @cp -rp lib/picolibc/libc/string build/release/tinygo/lib/picolibc/libc - @cp -rp lib/picolibc/libc/stdio build/release/tinygo/lib/picolibc/libc - @cp -rp lib/picolibc/libm/common build/release/tinygo/lib/picolibc/libm - @cp -rp lib/picolibc/libm/math build/release/tinygo/lib/picolibc/libm - @cp -rp lib/picolibc-stdio.c build/release/tinygo/lib + @cp -rp lib/picolibc/libc/ctype build/release/tinygo/lib/picolibc/libc + @cp -rp lib/picolibc/libc/include build/release/tinygo/lib/picolibc/libc + @cp -rp lib/picolibc/libc/locale build/release/tinygo/lib/picolibc/libc + @cp -rp lib/picolibc/libc/machine/xtensa build/release/tinygo/lib/picolibc/libc/machine + @cp -rp lib/picolibc/libc/stdlib build/release/tinygo/lib/picolibc/libc + @cp -rp lib/picolibc/libc/string build/release/tinygo/lib/picolibc/libc + @cp -rp lib/picolibc/libc/stdio build/release/tinygo/lib/picolibc/libc + @cp -rp lib/picolibc/libm/common build/release/tinygo/lib/picolibc/libm + @cp -rp lib/picolibc/libm/math build/release/tinygo/lib/picolibc/libm + @cp -rp lib/picolibc-stdio.c build/release/tinygo/lib @cp -rp lib/wasi-libc/dlmalloc/src build/release/tinygo/lib/wasi-libc/dlmalloc @cp -rp lib/wasi-libc/libc-bottom-half/cloudlibc build/release/tinygo/lib/wasi-libc/libc-bottom-half @cp -rp lib/wasi-libc/libc-bottom-half/headers build/release/tinygo/lib/wasi-libc/libc-bottom-half @@ -121,6 +124,7 @@ endif @cp -rp lib/wasi-libc/libc-top-half/musl/src/unistd build/release/tinygo/lib/wasi-libc/libc-top-half/musl/src @cp -rp lib/wasi-libc/libc-top-half/sources build/release/tinygo/lib/wasi-libc/libc-top-half @cp -rp lib/wasi-cli/wit build/release/tinygo/lib/wasi-cli/wit + @cp -rp lib/xtensa/include build/release/tinygo/lib/xtensa @cp -rp ${LLVM_PROJECTDIR}/compiler-rt/lib/builtins build/release/tinygo/lib/compiler-rt-builtins @cp -rp ${LLVM_PROJECTDIR}/compiler-rt/LICENSE.TXT build/release/tinygo/lib/compiler-rt-builtins @cp -rp src build/release/tinygo/src diff --git a/src/runtime/arch_xtensa.go b/src/runtime/arch_xtensa.go index f3192e000..dec6afe8f 100644 --- a/src/runtime/arch_xtensa.go +++ b/src/runtime/arch_xtensa.go @@ -11,7 +11,9 @@ const zeroSizeAllocPtr uintptr = 16 // part of early flash: partition table, etc // The bitness of the CPU (e.g. 8, 32, 64). const TargetBits = 32 -const deferExtraRegs = 0 +// Xtensa's windowed ABI uses a 17-word jmp_buf. JumpSP and JumpPC provide the +// first two words. +const deferExtraRegs = 15 const callInstSize = 3 // "callx0 someFunction" (and similar) is 3 bytes diff --git a/src/runtime/asm_xtensa.S b/src/runtime/asm_xtensa.S new file mode 100644 index 000000000..352fb32a4 --- /dev/null +++ b/src/runtime/asm_xtensa.S @@ -0,0 +1,34 @@ +#ifdef __XTENSA_WINDOWED_ABI__ +.section .text.__xtensa_libgcc_window_spill,"ax",@progbits +.global __xtensa_libgcc_window_spill +.type __xtensa_libgcc_window_spill, %function +__xtensa_libgcc_window_spill: + entry sp, 16 + movi a6, 15 + call4 .Lspill_windows + retw +.size __xtensa_libgcc_window_spill, .-__xtensa_libgcc_window_spill + +.balign 4 +.Lspill_windows: + entry sp, 16 + beqz a2, .Lspill_done + addi a2, a2, -1 + mov a6, a2 + call4 .Lspill_windows +.Lspill_done: + retw +#endif + +.section .text.tinygo_longjmp,"ax",@progbits +.global tinygo_longjmp +.type tinygo_longjmp, %function +tinygo_longjmp: +#ifdef __XTENSA_WINDOWED_ABI__ + // longjmp's entry rotates a10/a11 into its a2/a3 arguments. + movi a11, 1 +#else + movi a3, 1 +#endif + j longjmp +.size tinygo_longjmp, .-tinygo_longjmp diff --git a/targets/xtensa.json b/targets/xtensa.json index 893a3eb75..4e9f330b1 100644 --- a/targets/xtensa.json +++ b/targets/xtensa.json @@ -14,5 +14,6 @@ ], "ldflags": [ "--gc-sections" - ] + ], + "extra-files": ["src/runtime/asm_xtensa.S"] }