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
/ENDTRY
pairs, for example); - Allow throwing exceptions from any called function;
- Nested
try
blocks, 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
.c
files. The global variable you require should be in your library's.c
. The code fortry_throw()
andtry
should 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
throw
is not inside atry
/catch
block 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
int
s? 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
finally
like Java has. C++ doesn't need it since it has destructors; but since C doesn't have destructors,finally
would 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
test
directory 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.c
and link againsttry.o
if this fits better in the overall project structure.Adding
finally
and structured exceptions would require some more thoughts. I'll think about them (especially thefinally
block.I'll write another article to explain the inner working of
try.h
but 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 afinally
clause 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
return
orbreak
orgoto
is executed is too tricky (if at all possible!).As for having an object as exception, I'm not clear how the
catch
block 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
struct
literal 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
exception
allows access to this informationBy default,
expression_num
,file_name
, andline_num
are available but you can specify others. Thetest/test7.c
file 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
count
member.Since
try
is implemented as afor
instruction, 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 thefor
loop.Setting the
count
field to2
will 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
/catch
blocks, and we'll use the sign for this.If an exception was raised but no catch has been executed, the
count
field will be set to -2 (second loop but with no catch!) and theif
withtryabort()
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
enum
with 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 takevolatile
into 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_try
will be1
at the end. See here, this bullet:volatile
local variables in the function containing the invocation ofsetjmp
, whose values are indeterminate if they have been changed since thesetjmp
invocation.The
try
macro callssetjmp()
;throw
callslongjmp()
;setjmp()
returns a second time. At this point, the value ofn_try
is indeterminate. On my platform compiling with-O2
, the incremented value ofn_try
is lost since it was likely in a register.longjmp()
does not restore such registers, son_try
is still0
.The fix as alluded to is that you need to declare
n_try
to bevolatile
:The problem is that you need to declare all local variables that are declared outside of the
try
block but modified inside thetry
block 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
catch
block 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
exception
fields.And If I really, really, need to make sure that some of the changes are retained from the
try
to thecatch
block I'll have to set themvolatile
orglobal
. I agree that I should be more explicit about it, I only cited in passing in thetest7.c
code.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
count
was 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 ofn
is indeterminate. It might be the partial sum calculated so far (the useful result), or it could be0
. It's just too easy to forget declaringn
asvolatile
.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
volatile
issue 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 ofn
becomes (the vast majority of the times) irrelevant so it should be easy to spot those times when thecatch
block could use it for some recovery action and addvolatile
to 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
finally
as well.Nice. I see how you explored the limitations of the approach I followed.
There's little point to me in how
finally
can 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 afinally
block 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.c
in 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
f
gets closed even if you throw in itstry
block.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
&free
it (unless perhaps it's possible to have the implementationfree
it 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
rethrow
for this:Or even simpler:
To me,
finally
would 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 withfinally
and 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 thetry
block and in everycatch
block.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.