Crystal on Windows: A Deep Dive on Exception Handling
One of the many things Windows does differently to Linux and other UNIX-derived operating systems is how it handles exceptions. In the process of porting the Crystal compiler to Windows, I’ve had to learn a lot about how the Crystal compiler currently handles exceptions, how LLVM handles exceptions, and how these differ on Windows.
Exception handling is split into two parts: the raising of the exception (
raise function implementation), and the exception handler (how the compiler generates code for rescue blocks). The
raise function is runtime code implemented by Crystal’s standard library using libunwind, which looks at compile-time metadata to work out which handler to call. The exception handler generation is handled by the compiler with the help of LLVM, which generates the compile-time metadata used by
The job of the
raise function is to find the rescue block which was most recently entered and pass it the exception it was given as an argument. It does this by inspecting the call stack — the stack containing local variables and return addresses for every function. Ignoring the details of stack unwinding, you end up with a backtrace, a list of the function calls currently executing in the thread — familiar to anyone who’s dealt with unhandled exceptions before.
This is where the compile-time metadata comes in. For each method call in the backtrace, the compile-time metadata is queried for where the rescue block is (if any). For example in the code below, the metadata would contain a record saying that the exception handler for lines 2–4 was at line 6. Of course in reality, these are addresses of instructions, not line numbers.
Once the exception handler has been found, the exception handler’s job is to call the rescue and ensure blocks. This is where the compiler comes in, since the exception handler is generated by the compiler. Since there can be multiple rescue blocks with different type restrictions, and rescue blocks themselves can raise exceptions, how the compiler generates this code can get quite complex.
Internally, the compiler converts code like this:
into something similar to this:
The important part of this transformation is that now each of the two(!) rescue blocks have only one rescue clause with no type restrictions. Two rescue blocks are needed in the case of having both rescue and ensure blocks, because the ensure block needs to run if the rescue block raises.
Let’s see how the compiler represents this in terms of LLVM IR — the intermediate code that LLVM speaks which is between Crystal and assembly. We’ll take a look at a simple function and it’s expansion into (simplified) LLVM IR.
It’s not necessary to understand everything that’s going on here, but the two important parts to this article is that the
raise function is called using
invoke instead of
call, unlike the call to
invoke, you specify where to jump to when the call returns, but you also specify where to jump to when the call unwinds using
unwind label. All calls inside the body of the rescue block use
invoke instead of
call. This is translated by LLVM to normal call instructions plus the compile-time metadata used by the raise function.
In this example,
raise is a
NoReturn function, so the
invoke_out block of code contains a single instruction which tells LLVM that it’s unreachable. The exception handler mentioned above is represented here as the
rescue block. Exception handler blocks always start with a
landingpad instruction which tells LLVM the block is an exception handler, and tells LLVM the arguments that the raise function passes to the exception handler (in Crystal’s case, a pointer to a
LibUnwind::Exception struct, and the type ID of the exception).
On Windows, naturally, things are a little different: libunwind is not available, and unwinding and throwing exceptions is handled by Microsoft’s C standard library. The metadata and layout are different too, in fact different enough that LLVM had to introduce a completely different API for landing pads. Here is the exact same code compiled for windows:
Some things are the same, a lot of things are different. The same
invoke is used, however the
landingpad is gone, replaced with a
catchswitch points to one or more
catchpad instructions, which function a lot like landing pads. The difference here is that you can have multiple catch pads, which catch different types, whereas you can only have a single landing pad. This is slightly more efficient, but more complex to implement in the compiler, a decision likely taken since C++-based Windows SDKs tend to use exceptions more heavily than C-based UNIX SDKs.
catchpad instruction takes a list of arguments, a lot like a function call, but doesn’t return a value. This is unlike a landing pad which returns a user-defined type. The first argument is a descriptor of the type to be caught, here we use the type descriptor for
void*, to catch all exceptions. The second argument is the number zero (if you have an idea of the purpose, please leave a comment below). The third argument is a pointer to where to copy the exception object.
There are also some smaller differences, such as a
catchret instruction being required to exit the catch pad, whereas landing pads have no “exit” and can branch wherever they want. “funclet tokens” are also required on all
invoke instructions which execute inside a
catchpad. The details of this are explained in this handy LLVM guide.
I hope this article is interesting to people wanting to know a little more about how exceptions work, and perhaps good introductory reading to someone who wants to implement exception handling on Windows (although I’ve left some details out for clarity, check the actual commit for all the details). Now Windows exception handling is complete, the next part of the story is porting the compiler itself!