Swift on windows - question about signals and exceptions

I didn’t want to derail the review thread, but i wanted to learn more about signals and exceptions when it comes to Swift and windows support. @grynspan post talked about “we conflate” - I’m guessing that was meant to be the swift runtime on windows.

Any pointers to where to find that code to understand what’s happening, or how it might work better? And how signals could be managed more consistently across platforms (or what the “annoying” mismatches are that make this notably challenging?)

I was talking in particular about the current implementation of Process in swift-corelibs-foundation, which Subprocess would eventually replace.

On POSIX and POSIX-like systems (including Linux, Darwin, and FreeBSD) exit codes passed to exit() are distinguished from signal codes that cause processes to terminate. These two domains are easily distinguished using POSIX-layer API or using Process.terminationStatus and Process.terminationReason.

On Windows, signals are not a feature of the kernel and are emulated in userspace. If you trigger a signal in a Windows process (e.g. by calling abort() to raise SIGABRT), the system invokes the signal handler for that signal, and the default signal handlers on Windows all… call _Exit(3). Like, literally the value 3. There is then no API that a listening parent process can invoke to determine the signal that was raised—the API you'd use, GetExitCodeProcess(), just says "the process exited with code 3." Not terribly useful.

Windows also has a feature called Structured Exception Handling that… is sort of intertwined with C++ exceptions but is also distinct, and has various tendrils in the OS as well as in the Visual C++ compiler. I won't try to explain them further here, except to say that if a process raises a structured exception and doesn't handle it, the system terminates the process as if by calling _Exit(exceptionCode), where exceptionCode is a 32-bit value that encodes the specific exception that occurred. A listening parent process would again be told "the process exited with code exceptionCode", and there is no way to determine if exceptionCode is actually an exception code or if the child process just passed the value to exit().

Back to Process: the current Windows implementation of terminationStatus and terminationReason does not reliably tell you if a signal was raised. Instead, if the bits of the child process' exit code match one of the patterns Microsoft defines for unhandled structured exceptions, then the bits are unpacked, the low 30 (out of 32) bits are used as terminationStatus and terminationReason is set to .uncaughtSignal.

The result of all this rambling is that abort() on macOS or Linux is reported as terminationStatus == SIGABRT && terminationReason == .uncaughtSignal, while on Windows it's reported as terminationStatus == 3 && terminationReason == .exit, while terminationReason == .uncaughtSignal is used for an entirely different class of failure modes.

Swift Testing makes a "best effort" to rectify this situation. Since the child processes spawned by Swift Testing are controlled by it, on Windows we replace the default signal handlers (that exit with code 3) with ones that try to pack the original signal value into an otherwise-unused exception code before calling _Exit(); the parent process then knows how to recognize these exception codes and unpack them back into signals, so we get more reliable signal reporting on Windows (unless some other code in the child process resets the signal handler before raising, but again, "best effort.")

Hope that helps!

6 Likes

Thanks Jonathan! I appreciate all the detail you provide and the time you took to share it. Yes, it definitely helps!

1 Like