CException: Why?
CException is a tiny module for C that adds exception handling. I'm going to put together a short series here that covers everything I know about this useful framework. This is part 1, focusing on WHY someone would want to use it. If you're already onboard, feel free to jump to part 2 [here].
This exception handling isn't anything fancy like the stuff in C++, though it looks similar. It's certainly nothing spectacular like you would find in higher level languages. In some ways this is good. In others, not.
The purpose of CException is to provide a straightforward and reliable method of handling errors, without interrupting our normal flow. This is obvious when we have functions which return values that we care about.
EXAMPLE?
What we need is an example. Let's say we have a function that accepts a pointer to a null-terminated string. It's supposed to return an integer, assuming that the string represented a valid number.
Clearly, things could go wrong. Here are some thoughts off the top of my head:
- We could pass a NULL.
- We could pass a zero-length string.
- We could pass a number that is too big or too small to represent.
- We could pass strings that aren't numbers at all.
- We could pass strings that don't terminate in a reasonable amount of time.
But these cases are all exceptions to the normal flow of our program. Our normal flow is going to be something like this:
- Read in data from parser
- Convert to number
- Set our PWM to run at that duty cycle.
Let's look quickly at few common error handling options. None of them are ideal, including CException. Instead, we'll arm ourselves with some information so we can make good error-handling decisions in our own applications later.
RETURN CODES
If we have a policy of always using return codes, then we need to reshape our functions a bit to accommodate this. The nice thing about this method is that it is usually fairly efficient speedwise. The downside is that it litters are code with unnecessary indirection, constant error checking, and pointers where pointers wouldn't otherwise be needed.
For example, our sample above might look something like this:
int UpdateDutyCycle(void) {
char percent;
char* parsed;
int status;
status = ParseNext(&parsed);
if (status != 0)
return status;
status = ConvertToNum(parsed, &percent);
if (status != 0)
return status;
status = SetDutyCycle(percent);
return status;
}
So, I made the assumption we want to pass back an error code. I mean... getting details is good, right? But let's say you wanted to be lazy and just go with a true/false kind of scenario. I'm sure you can see it doesn't really simplify this situation much.
This code is ugly. Despite the fact that it's really just 3 steps, we're looking at a dozen lines of code... most of which is error handling. This type of code is fine when we really need the extra horsepower, but it's pretty poor in the self-documenting and maintenance areas.
ERROR FUNCTIONS
A close relative of our last example is the error function. But this isn't one of those relatives you should be looking forward to seeing. This is your curmudgeony Great Aunt Helga whose favorite past-time is asking you why you don't settle down with someone nice for a change... in front of your S.O.
Error functions work like this: All the functions in a module have the ability to tell a central status handler that they ran into trouble. Then you can ask for that error from a single place.
This just encourages developers to never check their errors, though. Developers look at each function and they're playing a risk-analysis based on a gut reaction. "Is ParseNext likely to ever have an error? Maybe I can just skip the error handler for that one? What about the SetDutyCycle? I know I'm going to just send it values between 0 and 100, so it should always be fine, right?"
The problem is that their gut is going to be wrong sometimes... and then you have errors that are going uncaught.
Or let's say they DO check each value. Then they have something that looks like the following:
int UpdateDutyCycle(void) {
char percent;
char* parsed;
int status;
parsed = ParseNext();
status = CheckStatus();
if (status != 0)
return status;
percent = ConvertToNum(parsed);
status = CheckStatus();
if (status != 0)
return status;
SetDutyCycle(percent);
return CheckStatus();
}
That's worse than the return codes, right? It's more verbose, has many more function calls (making it much slower). I personally don't think there is ever a situation where this is the right handler.
TUPLES
Saying a function returns a "Tuple" is just language-nerd speak for saying that the function can return more than one value at once. (As a language-nerd, it's not offensive when I say that, right?)
How is this done in C?
Most often, with structs. Instead of returning just the value we care about, we return a struct which contains the value AND a status code.
There are rare occasions where this is an efficient way to go (most often when you were already returning a struct which is specific to this function already). But otherwise, I think you'll quickly see that it creates a situation much like the Error Function method above:
int UpdateDutyCycle(void) {
CONVERT_RETVAL_T percent;
PARSE_RETVAL_T parsed;
parsed = ParseNext();
if (parsed.status != 0)
return parsed.status;
percent.status = ConvertToNum(parsed.val);
if (percent.status != 0)
return percent.status;
return SetDutyCycle(percent.val);
}
RESERVED VALUES
From my experience, Return Codes and Reserved Values are roughly tied in how often they are used in Embedded Software applications. Reserved Values are fast and efficient. They allow us to return the value directly, which is handy. Their syntax is often slightly cleaner than our previous methods:
int UpdateDutyCycle(void) {
char percent;
char* parsed;
parsed = ParseNext();
if (parsed == NULL)
return ERROR_WITH_PARSING;
percent = ConvertToNum(parsed);
if (percent > 100)
return percent;
return SetDutyCycle(percent);
}
For this particular application, this is probably passable. We can return a NULL from parsing if anything goes wrong (though we can't say anything about WHAT went wrong this way, which is unfortunate). There is plenty of space above the valid percent values to store error codes too... though it may or may not be valid to just pass those along as I've done here. But all in all, this method isn't awful... for this situation.
It should be noted that there are a lot of situations where this would not be true. What if our parser needed to return any valid number? Then we wouldn't have any reserved values to stuff errors into. And, as you can see, we're still left checking some sort of status after every single function call.
EXCEPTIONS
So finally, we've reached exceptions. Let's start with the good. If we're using exceptions, our function is going to look something like this:
int UpdateDutyCycle(void) {
CEXCEPTION_T e;
char percent;
char* parsed;
Try
{
parsed = ParseNext();
percent = ConvertToNum(parsed);
SetDutyCycle(percent);
}
Catch(e)
{
return e;
}
return STATUS_OK;
}
Actually, that's assuming we need to return an error code from UpdateDutyCycle... but if we're using CException, that error is just going to propagate up our call stack until the last Try block that we defined... so since UpdateDutyCycle isn't the place where we are handling the error, it just needs to pass it along (which means it needs to do nothing about it).
So let's collapse this function down a bit:
void UpdateDutyCycle(void) {
char percent;
char* parsed;
parsed = ParseNext();
percent = ConvertToNum(parsed);
SetDutyCycle(percent);
}
And, just because it's fun, we could actually collapse this down further (depending on how clear it ends up):
void UpdateDutyCycle(void)
{
SetDutyCycle( ConvertToNum( ParseNext() ) );
}
Even without shrinking it down this far, I think most will agree that using an exception framework makes the main flow of code much more clear. We can focus on the important flow, and trust that errors are just being handled. How is that happening? Well, inside each of our functions, if a problem is found, CException throws the error. Execution of that function immediately stops and the stack unwinds back to the most recently nested Catch function. The thrown ID is then given to the variable in Catch.
Part of ConvertToNum probably looks like this:
int ConvertToNum(const char* str) {
if (str == NULL)
Throw(ERROR_STRING_NULL);
if (str[0] == 0)
Throw(ERROR_STRING_EMPTY);
//code that actually does the parsing
return parsed_val;
}
As you can see, functions can throw a specific error code for their particular problem. This allows developers to focus in on problems very quickly.
When this happens, the rest of ConvertToNum isn't executed. The rest of UpdateDutyCycle isn't called. This continues until we reach a calling function that had a Try... Catch block. Let's say it's our main function:
void main(void) {
CEXCEPTION_T e;
//some init stuff is here
while(1) {
Try
{
UpdateDutyCycle();
HandleUsbUpdates();
BlinkAwesomeLights();
}
Catch(e)
{
DebugUsartWrite("Error Encountered %i", e);
while(1); //loop forever
}
}
}
This example may be an extreme case. A single bad byte causes us to deadloop the program? Probably not the wisest of decisions... but I think you can see how exception handling works now. What if you wanted to just handle problems in UpdateDutyCycle a bit more gently, and then let other problems be handled here? No problem, just add a Try...Catch back into UpdateDutyCycle. Handle that error however you like.
THE BAD
There is obviously a downside to using CException.
First, it's slower than most of the other methods in shallow cases (meaning where your call stack is only a function call or two). That's because each Try... Catch block actually takes a snapshot of your register set and pushes it on the stack. That snapshot is used if you ever Throw to get immediately back to the previous state.
The good news here: As long as you aren't nesting many many Try... Catch blocks (which you honestly rarely will... the whole point is to centralize your error handling!), CException starts to get faster than the other error handling methods as your codebase grows.
The other downside to CException is stack use. Each time you enter a nested Try...Catch block, you allocate your entire register set on the stack. It is released again when you exit the block. So, like the speed issue, this issue is really a factor of how many layers of Try...Catch you will nest.
In my experience working with some very large projects, I spend most of my time at 2 levels deep and occasionally reach 3. Only in rare circumstances does my CException stack get to 4 or more levels. (Why 2? Because I always have a global fallback handler for uncaught exceptions, and then normal exception handlers one level in).
UP NEXT
How does it work? For that, you'll have to tune in to part 2 of this series!