Under the Hood: CException
We've been talking about CException. In the first part of this series, we discussed WHY you would want to use CException, and WHEN it is most appropriate. Today, we're going to dissect it so that we understand exactly how it works. We'll discuss how to avoid common pitfalls in the next (and final) part of this series.
SETJMP / LONGJMP
To understand CException, you need to understand a little about the standard C library setjmp. This library is a part of most standard C implementations (it's part of the ANSI standard, after all). It's purpose is to provide "non-local gotos". Sound scary? Well, it can be, if you aren't careful.
This library consists primarily of two functions. The function setjmp saves your current register set and environment, including the stack pointer. Then, if we've just called this function, it returns 0.
The second function is longjmp. This function stops the current flow of execution immediately. It unwinds the stack back to the the most recent call of setjmp, restoring all local environment and registers. It resumes execution from the point where you called *setjmp* most recently. Instead of returning 0 this time, however, setjmp returns whatever number we passed to longjmp!
Sounds kinda like exception handling, doesn't it? This is a rough predecessor the the exception handling found in higher level languages.
So, we take this standard library and add a thin wrapper around it. If the library is our scaffolding, this little wrapper adds some extra life to it and makes it more attractive in a number of ways.
1 UP: DRY WALL
A wall isn't a wall if it's just a bunch of two by fours spaced apart. We need something more to turn it into something functional.
In the same way, we don't have an exception handling system with just setjmp. To have a real exception handling system, we need to manage these environment snapshots. If we only have one instance, we can only have one error handler. What happens if we want to nest our handlers? What happens if we want to use our error handling in a multitasking environment?
CException does this through a careful coordinated effort between module and local variables. It has a single module-scoped pointer to the current environment frame (or, if multi-tasking, it has a pointer per task, stored in a static array). Right before we call setjmp, we declare a local struct on the stack to store the environment variables. We also declare a pointer to the previous environment snapshot. We copy the current pointer to our previous pointer for safe keeping, then assign the current pointer to look at our new environment frame. Then we can call setjmp with our brand new frame, allowing it to fill it with the latest details.
When we leave setjmp, clearly want to handle our error, if there is one, or move on. Before we do either, however, we restore our current pointer to point at the previous, now that we're done with our new frame. Because both the previous pointer and the new environment struct were local variables, they will be released automatically when they go out of scope. No muss, no fuss.
2 UP: SPACKLE OVER THE DINGS AND DENTS
But wait, there's another free life for us! It's very important that all of the steps above happen. If we don't create a new frame when we next our setjmp calls, we're going to run into big trouble when our nested frame goes out of scope. If we attempt to longjmp to an invalid frame (possibly because we haven't created one with setjmp yet), things are going to blow up. The setjmp library alone can be a dangerous tool.
Much of CException's purpose to cover these trouble areas so that no one needs to deal with them. It structures its macros in a way that the compiler with raise an error if vital parts are missing. It handles all the memory under the hood, so that the user doesn't have to worry about it. It even manages using setjmp across multiple tasks with just a single implementation-specific hook required.
3 UP: FRESH PAINT
Finally, CException makes the error handling of setjmp / longjmp act similar to common programming conventions. It introduces a Try { ... } Catch(e) { ... } style of protection. Everything within the Try braces are protected by setjmp. If anything bad happens (anything calls Throw), then execution will immediately return to the Catch braces. The local variable e will automatically be assigned the error ID that was given to Throw... even if that Throw call was a dozen functions away in the call stack.
RESTORE FROM SAVE POINT?
There you go. You now know how CException works. If you want a line by line, you can crack open the C file. You'll find that there is a line by line commented description of what everything is for. You'll also find that CException is indeed a very small wrapper. One function. Two macros. A handful of configuration options. Done.
Now that you understand how it works, you might be wondering how best to use it. Maybe you've identified a couple of the potential gotchas. Come on back for the last part of this series. I'll have some tips for you!