From 8ffabbea646fe2be68c0561455eb137b47958337 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 10 May 2026 15:56:22 -0700 Subject: [PATCH] runtime: add Windows vectored exception handler for recoverable panics Register a vectored exception handler at startup so Windows hardware exceptions can be translated into Go panics. Access violations become nil pointer panics, and integer divide-by-zero exceptions become divide by zero panics, allowing defer/recover to handle them like ordinary runtime panics. --- compileopts/target.go | 2 ++ src/runtime/runtime_windows.c | 29 +++++++++++++++++++++++++++++ src/runtime/runtime_windows.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/runtime/runtime_windows.c diff --git a/compileopts/target.go b/compileopts/target.go index 0900a7ba2..70c704746 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -485,6 +485,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) { "--no-insert-timestamp", "--no-dynamicbase", ) + spec.ExtraFiles = append(spec.ExtraFiles, + "src/runtime/runtime_windows.c") case "wasm", "wasip1", "wasip2": return nil, fmt.Errorf("GOOS=%s but GOARCH is unset. Please set GOARCH to wasm", options.GOOS) default: diff --git a/src/runtime/runtime_windows.c b/src/runtime/runtime_windows.c new file mode 100644 index 000000000..4498d0f0b --- /dev/null +++ b/src/runtime/runtime_windows.c @@ -0,0 +1,29 @@ +//go:build none + +// This file is included on Windows (despite the //go:build line above). + +#include +#include + +void tinygo_sigpanic_windows(int32_t exception_code); + +static LONG WINAPI tinygo_exception_handler(EXCEPTION_POINTERS *info) { + DWORD code = info->ExceptionRecord->ExceptionCode; + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_INT_OVERFLOW: + tinygo_sigpanic_windows((int32_t)code); + // If runtimePanic triggers longjmp, we never reach here. + // If it doesn't (no defer frame), it will abort and we also + // never reach here. + return EXCEPTION_CONTINUE_SEARCH; + default: + return EXCEPTION_CONTINUE_SEARCH; + } +} + +void tinygo_init_exception_handler(void) { + AddVectoredExceptionHandler(1, tinygo_exception_handler); +} diff --git a/src/runtime/runtime_windows.go b/src/runtime/runtime_windows.go index 8419fb4f8..839356510 100644 --- a/src/runtime/runtime_windows.go +++ b/src/runtime/runtime_windows.go @@ -60,6 +60,10 @@ func mainCRTStartup() int { _QueryPerformanceFrequency(&performanceFrequency) } + // Register vectored exception handler so that access violations and + // divide-by-zero exceptions can be recovered with defer/recover. + tinygo_init_exception_handler() + // Obtain the initial stack pointer right before calling the run() function. // The run function has been moved to a separate (non-inlined) function so // that the correct stack pointer is read. @@ -294,3 +298,27 @@ func hardwareRand() (n uint64, ok bool) { // //export SystemFunction036 func _RtlGenRandom(buf unsafe.Pointer, len int) bool + +const ( + _EXCEPTION_ACCESS_VIOLATION = 0xC0000005 + _EXCEPTION_IN_PAGE_ERROR = 0xC0000006 + _EXCEPTION_INT_DIVIDE_BY_ZERO = 0xC0000094 + _EXCEPTION_INT_OVERFLOW = 0xC0000095 +) + +//export tinygo_init_exception_handler +func tinygo_init_exception_handler() + +//export tinygo_sigpanic_windows +func tinygo_sigpanic_windows(exceptionCode int32) { + switch uint32(exceptionCode) { + case _EXCEPTION_ACCESS_VIOLATION, _EXCEPTION_IN_PAGE_ERROR: + runtimePanic("nil pointer dereference") + case _EXCEPTION_INT_DIVIDE_BY_ZERO: + runtimePanic("divide by zero") + case _EXCEPTION_INT_OVERFLOW: + runtimePanic("integer overflow") + default: + runtimePanic("unknown exception") + } +}