Don't worry, this is not an article about how great the C language is. No flames ahead. This is about how I added exception handling to C in 100 lines of code, comments included (available here: https://github.com/rdentato/try ).
Exceptions are a way to signal that something unexpected happened and some special measures need to be taken to be able to continue the program (or just exit the program if there is no way the program can execute).
Intended as a way to cleanly handle errors, they have been, sometimes, streteched to become a sort of glorified GOTO which led to some suspicion on their effectiveness (we all know that goto is evil, right?).
The idea is simple: when an exception is thrown, the control jumps back to a specified portion of the program, where the exception is caught, analyzed and proper actions are taken.
Languages that offers exceptions (C++, Java, Python, ...) do it with the try/catch instructions. Something like:
try {
... Your code here with calls to distant functions
}
catch {
... The code that is executed if an exception has been thrown
... by the code in the try block or any function called by it.
}
Error handling in C, instead, relies on functions returning error codes and/or setting the variable errno. It will be up to the caller to determine if an error occurred, taking the right measures and possibly propagating it up in the calling chain in a meaningful way.
This can be not an easy task because, to do it right, you need to properly plan how the errors will propagate in your applications and you may end up with one of this two, unfavourable, scenarios:
- The overall logic gets more complicated just to gracefully handle errors;
- Errors that are really exceptional (e.g. no more memory, file systems full, ...) cause the program to exit or, even, are not handled at all.
I do claim that there are situations where a C program could be written better (i.e. it would easier to write and to mantain) if we only had exceptions in C.
We don't have the try/catch block in C but we do have something that can serve the same purpose: setjmp()/longjmp().
They allow a non local goto: you set the return point with setjmp() and get back to it with a longjmp().
This mechanism is quite seldom used (and quite rightly so!) and involves saving the status in a variable of type jmp_buf that must be passed around to allow longjmp() to jump to the set return point in the code. Not exactly simple or clean.
I wanted to have something simple to use and that's why I created my own version of the try/catch instructions which you can find here: https://github.com/rdentato/try .
You can find on the Internet other implementations of try/catch in C (also based on setjmp()/longjmp()) but I couldn't find one that had all the features I wanted:
- Fit nicely in the C syntax (no ugly
STARTRY/ENDTRYpairs, for example); - Allow throwing exceptions from any called function;
- Nested
tryblocks, even within called functions; - Information (file and line) about where the exceptions was thrown, for better error messages.
- Thread safe. (not really a priority for me, but nice to have)
About the last point, the better I could do is to be able to have try/catch working in a single thread but not across threads, which would have been extremely confusing!
Overall, I'm pleased with the result and wanted to share it with you all. Let me know in the comments below if you want me to write a followup on how it works internally, I'll be happy to write one.
Here is how exception handling looks like, you can find real example in the test directory on Github.
#include "try.h"
#define OUTOFMEM 1
#define WRONGINPUT 2
#define INTERNALERR 3
try_t catch = 0; // Initialize the exception stack
void some_other_func()
{
... code ...
if (invalid(getinput())) throw(WRONGINPUT);
... code ...
}
int some_func()
{
... code ...
try {
... code ...
if (no_more_memory) throw(OUTOFMEM)
some_other_func(); // you can trhow exceptions
// from other functions
... code ... // You MUST NEVER jump out of a try/catch block
// via return, goto or break.
}
catch(OUTOFMEM) {
... code ...
}
catch(WRONGINPUT) {
... code ...
}
catch() { // catch any other exception
... code ... // otherwise the program will abort.
}
... code ...
}
Top comments (22)
This article would have been a lot better is if you discussed how it's implemented.
I also don't understand your aversion to putting non-trivial code into
.cfiles. The global variable you require should be in your library's.c. The code fortry_throw()andtryshould also be in the.c.You could also use more meaningful names. Rather than something like:
why not:
? It seems odd to use a short, cryptic name — where you add a comment where it's declared — when you could have just given it a more descriptive name — and then you wouldn't need the comment!
It's also not clear whether you can do cross-function
try/catch/throw, e.g.:That is, where the call to
throwis not inside atry/catchblock of its own, but inside of a caller's.When an exception is thrown with no
try/catch, you likely could do better withtry_abort()to include a message along with the crash.Why not use exception objects rather than simple
ints? Then the user could "construct" exception objects:(I'm not certain it's possible, but maybe try it?)
You could also see if you could implement
finallylike Java has. C++ doesn't need it since it has destructors; but since C doesn't have destructors,finallywould be nice to have.Yes, you can throw an exception from within a function called in the try block and the proper catch will handle it.
You can also nest try blocks and rethrow the same exception to let the parent handle it. I've added some more info in the header comment.
The examples in the
testdirectory check for some quite convoluted scenarios.As for naming, if you look at the repo now, you'll see I modified the names as you suggested, I'm too used to writing code for myself it seems :) Thanks for the feedback.
It's my preference to only have to include a single file in my projects, that's why I tend to use headers the way I do. However, I just added back the possibility to compile
try.cand link againsttry.oif this fits better in the overall project structure.Adding
finallyand structured exceptions would require some more thoughts. I'll think about them (especially thefinallyblock.I'll write another article to explain the inner working of
try.hbut it will require some time.About
finally. Since I have introducedleave()and the rule that one should always exit from a try/catch block either because the block is ended or byleave(). Adding afinallyclause would be useless since the code after the block will be executed regardless an exception has been thrown or not.Having code executed even if a
returnorbreakorgotois executed is too tricky (if at all possible!).As for having an object as exception, I'm not clear how the
catchblock should be structured .Maybe I could replace the current variable that is set to errno with a full structure like:
and throw an exception with:
and later:
This would keep the overall structure simple (you may or may not specify a struct with additional information) but will provide more flexibility.
Would that reach the goal you had in mind? Forcing exceptions to always be structures seems to make things more complex with no real benefit (that I can see).
I think real-world code would always need additional information with an exception. You could predefine some structures in your library that the user can use if they wish if they only have simple things, e.g.:
Or maybe you could even use your code that implements "any type" in C using a
union.If you don't like C's
structliteral syntax, you can always add macros:Then:
Following your suggestion, I added a way to specify (and retrieve) additional information about exception.
A new object called
exceptionallows access to this informationBy default,
expression_num,file_name, andline_numare available but you can specify others. Thetest/test7.cfile has it explained.As an example here is what it looks like:
Also, instead of positionally you can use the field names to specify just some of the information:
@pauljlucas. Also, now you can specify your own function to be called upon an unhandled exception:
I don't quite get why you need the
countmember.Since
tryis implemented as aforinstruction, we need to count how many times we executed the loop's body (which is a chain ofif/else). We only need to execute the body once, the second time we must exit theforloop.Setting the
countfield to2will signal that an exception has been caught and that the next one will be the second pass in the loop.We also need to keep track of whether we executed any of the
try/catchblocks, and we'll use the sign for this.If an exception was raised but no catch has been executed, the
countfield will be set to -2 (second loop but with no catch!) and theifwithtryabort()will be executed.It's a bit convoluted but if you follow the execution step by step should become clearer.
I think you could probably use an
enumwith various states of the exception handling and transition between those. That would be a lot clearer.Not sure it will help much. Following the flow is complicated due to the setjmp/longjmp which may, or may not, reset the local variables.
Probably adding a comment explaining the flow would be better.
I've been playing around with my own implementation based on yours. It turns out your code has undefined behavior. It is only permissible to use
setjmp()as shown here. In particular, you can not assign the return value ofsetjmp()to a variable. You also don't takevolatileinto consideration.Did you ever try building yours with
-O2? I get incorrect results with my code even though it works fine with-O0.The problem is local variables in the stack frame of the function are indeterminate:
That code does not guarantee that
n_trywill be1at the end. See here, this bullet:volatilelocal variables in the function containing the invocation ofsetjmp, whose values are indeterminate if they have been changed since thesetjmpinvocation.The
trymacro callssetjmp();throwcallslongjmp();setjmp()returns a second time. At this point, the value ofn_tryis indeterminate. On my platform compiling with-O2, the incremented value ofn_tryis lost since it was likely in a register.longjmp()does not restore such registers, son_tryis still0.The fix as alluded to is that you need to declare
n_tryto bevolatile:The problem is that you need to declare all local variables that are declared outside of the
tryblock but modified inside thetryblock to bevolatile. This is a very high price to pay. The programmer could easily forget to do so.Using
setjmp()andlongjmp()can't really be used to implement a general exception-handling mechanism in C.@pauljlucas, thanks for having analyzed it so thoroughly.
I always considered the limitations on assigning the return value of
setjmp()quite useless and forgot about them. I feel that if assigning the value ofsetjmp()didn't work, many other things would break. But standards are standards and must be adhered to. I'm sure that if they put these restrictions, out there in the wild there is some compiler/architecture that needs themIf you look at the code now, there's just:
which is fully compliant.
As for the local variables, being try/catch an error handling mechanism (and not a control flow mechanism) I find it normal that they can't be (and shouldn't be) trusted when accessed in the catch block and, in general, after an exception has been thrown. Something failed, and the
catchblock is there exactly to ensure that the state (including the local variables) is set in a way that allows the execution to continue (if at all possible).If I need to pass additional information from the try to the catch block I now have the additional
exceptionfields.And If I really, really, need to make sure that some of the changes are retained from the
tryto thecatchblock I'll have to set themvolatileorglobal. I agree that I should be more explicit about it, I only cited in passing in thetest7.ccode.So, I still believe that setjmp/longjmp are perfectly fine to implement try/catch in C this is just getting better and better thanks to your feedback :)
P.S. I took the opportunity to simplify the state management as you indicated that using
countwas too confusing.I guess we'll have to agree to disagree. It should be fine to write code like:
But if
g()throws, the value ofnis indeterminate. It might be the partial sum calculated so far (the useful result), or it could be0. It's just too easy to forget declaringnasvolatile.Such an exception implementation in C gets you points for being clever and it's a fun intellectual challenge (indeed, I spent a few days on my own implementation seeing if I could improve on it), but combining the
volatileissue with the prohibition of callingbreak,continue, orreturn(all of which are also too easy to do), actually using this C exception code in production software is just too error prone. It's very likely that even reviewers of the code would miss such mistakes.Yes, we agree that we disagree :)
In my mind, if
g()throws an error, the value ofnbecomes (the vast majority of the times) irrelevant so it should be easy to spot those times when thecatchblock could use it for some recovery action and addvolatileto its declaration.This code is nowhere in production, not sure if it will ever be, but if you (or anyone else) have any idea on how to improve it I'll be happy to hear.
It is now a thousand times better than it was before your feedback.
FYI, my implementation is here. I added
finallyas well.Nice. I see how you explored the limitations of the approach I followed.
There's little point to me in how
finallycan be implemented in this context as whether it is there or not, the code after the try/catch blocks will be executed. Enclosing it into afinallyblock does not add much.Very nice idea to have groups of exceptions via a custom matcher! I've implemented something similar based on this idea. Now you can pass a function to
catch, instead of an integer, and if it returns a non-zero value, the exception is caught (seetest8.cin the test directory). Thanks for the idea!If I get it right, you did not implement the extended fields for the exceptions, nor the handling for abort(). I guess the point was just to find the limits of the approach, right?
I see you did a much better job than me at checking support for the thread local variable but I can't copy your code as it is GPL'd :(.
On a side note, I noticed you have your header for creating testing, I just happen to have put on GitHub my own implementation of a minimal test framework, I'd love to hear your comments.
That's not the point. If you have:
then
fgets closed even if you throw in itstryblock.I wasn't happy with your implementation of the extended fields. I was thinking more to have a
void *user_data, but then the user has tomalloc&freeit (unless perhaps it's possible to have the implementationfreeit automatically once the exception handling concludes — I didn't think about it that much). I wanted to spend a bit of time implementing it, but not over-engineering it since it's not clear if anybody will ever use this code, including me.Perhaps I misunderstand, but I don't understand the abort.
You can if your code is GPL'd too. I haven't decided whether to change it to LGPL — but that really doesn't change anything for you.
It's definitely much less minimal than mine. Mine is about as simple as you can get and seems sufficient for my purposes.
I believe it is better to have a default catch. I found it clearer when exceptions are handled where the try block is defined, and if the exception has to be propagated up to the parent, there's
rethrowfor this:Or even simpler:
To me,
finallywould be meaningful to have if we could exit from the blocks with break, return etc. But it's a matter of preference. Unless I missed something that could be done withfinallyand couldn't be done with my implementation.So far I only used MIT license or similarly liberal licenses. GPL, I understand for big projects but for libraries (and for such small libraries like mine) I see no incentive for me to use it.
I look forward to seeing how you'll implement the extended fields of exceptions. I'm interested.
Using
finally, the file is closed even if no exception is thrown since you always want to close the file. Otherwise, you have to callfclose()twice: once at the end of thetryblock and in everycatchblock.You can exit from the blocks by using
continue.You might have to wait a while.
Beautiful. This implementation clearly articulates my love for C. And what an impressive "extension" to the language!
I also really dig your code style -- it is concise, clear, yet highly functional.
Well done!
PS. I would love to read your love-letter to C. It too is my favourite colour/flavour/tone.