There is a constant problem in C that you have been dancing around but which I am going to solve in this exercise using a set of macros I developed. You can thank me later when you realize how insanely awesome these macros are. Right now you won't realize how awesome they are, so you'll just have to use them and then you can walk up to me one day and say, "Zed, those Debug Macros were the bomb. I owe you my first born child because you saved me a decade of heartache and prevented me from killing myself more than once. Thank you good sir, here's a million dollars and the original Snakehead Telecaster prototype signed by Leo Fender."
Yes, they are that awesome.
In almost every programming language handling errors is a difficult activity. There's entire programming languages that try as hard as they can to avoid even the concept of an error. Other languages invent complex control structures like exceptions to pass error conditions around. The problem exists mostly because programmers assume errors don't happen and this optimism infects the type of languages they use and create.
C tackles the problem by returning error codes and setting a global errno value that you check. This makes for complex code that simply exists to check if something you did had an error. As you write more and more C code you'll write code with the pattern:
This means for every function call (and yes, every function) you are potentially writing 3-4 more lines just to make sure it worked. That doesn't include the problem of cleaning up all of the junk you've built to that point. If you have 10 different structures, 3 files, and a database connection, when you get an error then you would have 14 more lines.
In the past this wasn't a problem because C programs did what you've been doing when there's an error: die. No point in bothering with cleanup when the OS will do it for you. Today though many C programs need to run for weeks, months, or years and handle errors from many different sources gracefully. You can't just have your webserver die at the slightest touch, and you definitely can't have a library you've written nuke a the program its used in. That's just rude.
Other languages solve this problem with exceptions, but those have problems in C (and in other languages too). In C you only have one return value, but exceptions are an entire stack based return system with arbitrary values. Trying to marshal exceptions up the stack in C is difficult, and no other libraries will understand it.
The solution I've been using for years is a small set of "debug macros" that implement a basic debugging and error handling system for C. This system is easy to understand, works with every library, and makes C code more solid and clearer.
It does this by adopting the convention that whenever there's an error, your function will jump to an "error:" part of the function that knows how to cleanup everything and return an error code. You use a macro called check to check return codes, print an error message, and then jump to the cleanup section. You combine that with a set of logging functions for printing out useful debug messages.
I'll now show you the entire contents of the most awesome set of brilliance you've ever seen:
Yes, that's it, and here's what every line does:
Here's an example of using all of dbg.h in a small program. This doesn't actually do anything but demonstrate how to use each macro, but we'll be using these macros in all of the programs we write from now on, so be sure to understand how to use them.
Pay attention to how check is used, and how when it is false it will jump to the error: label to do a cleanup. The way to read those lines is, "check that A is true and if not say M and jump out."
When you run this, give it some bogus first parameter and you should see this:
See how it reports the exact line number where the check failed? That's going to save you hours of debugging later. See also how it prints the error message for you when errno is set? Again, that will save you hours of debugging.
It's now time for you to get a small introduction to the CPP so that you know how these macros actually work. To do this, I'm going to break down the most complex macro from dbg.h and have you run cpp so you can see what it's actually doing.
Imagine I have a function called dosomething() that return the typical 0 for success and -1 for an error. Every time I call dosomething I have to check for this error code, so I'd write code like this:
What I want to use the CPP for is to encapsulate this if-statement I have to use all the time into a more readable and memorable line of code. I want what you've been doing in dbg.h with the check macro:
This is much clearer and explains exactly what's going on: check that the function worked, and if not report an error. To do this, we need some special CPP "tricks" that make the CPP useful as a code generation tool. Take a look at the check and log_err macros again:
The first macro, log_err is simpler and simply replace itself with a call to fprintf to stderr. The only tricky part of this macro is the use of ... in the definition log_err(M, ...). What this does is let you pass variable arguments to the macro, so you can pass in the arguments that should go to fprintf. How do they get injected into the fprintf call? Look at the end to the ##__VA_ARGS__ and that's telling the CPP to take the args entered where the ... is, and inject them at that part of the fprintf call. You can then do things like this:
log_err("Age: %d, name: %s", age, name);
The arguments age, name are the ... part of the definition, and those get injected into the fprintf output to become:
See the age, name at the end? That's how ... and ##__VA_ARGS__ work together, and it will work in macros that call other variable argument macros. Look at the check macro now and see it calls log_err, but check is also using the ... and ##__VA_ARGS__ to do the call. That's how you can pass full printf style format strings to check, which go to log_err, and then make both work like printf.
Next thing to study is how check crafts the if-statement for the error checking. If we strip out the log_err usage we see this:
if(!(A)) { errno=0; goto error; }
Which means, if A is false, then clear errno and goto the error label. That has check macro being replaced with the if-statment so if we manually expanded out the macro check(rc == 0, "There was an error.") we'd get:
What you should be getting from this trip through these two macros is that the CPP replaces macros with the expanded version of their definition, but that it will do this recursively, expanding all the macros in macros. The CPP then is just a recursive templating system, as I mentioned before. Its power comes from its ability to generate whole blocks of parameterized code thus becoming a handy code generation tool.
That leaves one question: Why not just use a function like die? The reason is you want file:line numbers and the goto operation for an error handling exit. If you did this inside a function, you wouldn't get a line number for where the error actually happened, and the goto would be much more complicated.
Another reason is you still have to write the raw if-statement, which looks like all the other if-statements in your code, so it's not as clear that this one is an error check. By wrapping the if-statement in a macro called check you make it clear that this is just error checking, and not part of the main flow.
Finally, CPP has the ability to conditionally compile portions of code, so you can have code that's only present when you build a developer or debug version of the program. You can see this already in the dbg.h file where the debug macro has a body only if it's asked for by the compiler. Without this ability, you'd need a wasted if-statement that checks for "debug mode", and then still wastes CPU doing that check for no value.
You can sign up for a video course at:
http://www.udemy.com/learn-c-the-hard-way/
This course is currently being built at the same time that the book is being built, but if you sign up now then you get early access to both the videos and PDF of the book.