DEV Community

Cover image for Exceptional C

Exceptional C

Remo Dentato on January 06, 2021

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 li...
Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

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 for try_throw() and try should also be in the .c.

You could also use more meaningful names. Rather than something like:

  unsigned short   ex;  // Exception number
Enter fullscreen mode Exit fullscreen mode

why not:

  unsigned short   exception_no;
Enter fullscreen mode Exit fullscreen mode

? 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.:

void f() {
    // ...
    if ( oops )
        throw( OOPS );
}

void g() {
    try {
        f();
    }
    catch ( OOPS ) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

That is, where the call to throw is not inside a try/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 with try_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:

struct my_exception {
    int foo;
};
typedef struct my_exception my_exception;

void f() {
    throw( (my_exception){ .foo = 42 } );
}
Enter fullscreen mode Exit fullscreen mode

(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.

Collapse
 
rdentato profile image
Remo Dentato • Edited

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 against try.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 the finally block.

I'll write another article to explain the inner working of try.h but it will require some time.

Collapse
 
rdentato profile image
Remo Dentato

About finally. Since I have introduced leave() and the rule that one should always exit from a try/catch block either because the block is ended or by leave(). Adding a finally 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 or break or goto is executed is too tricky (if at all possible!).

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

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:

   typedef struct my_exception {
      int foo;
      char * bar;
   } my_exception;
Enter fullscreen mode Exit fullscreen mode

and throw an exception with:

   throw( OUT_OF_MEM, (my_exception){ .foo = 42, .bar = "pippo"});
Enter fullscreen mode Exit fullscreen mode

and later:

  catch(OUT_OF_MEM) {
    my_exception *ex_info= throwninfo();
     printf("%d, %s\n", ex_info->foo, ex_info->bar);
  }
Enter fullscreen mode Exit fullscreen mode

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).

Thread Thread
 
pauljlucas profile image
Paul J. Lucas • Edited

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.:

struct int_exception {
    int value;
};
typedef struct int_exception int_exception;

struct ptr_exception {
    void *value;
};
typedef struct ptr_exception ptr_exception;
Enter fullscreen mode Exit fullscreen mode

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:

#define new_int_exception(VALUE) ((int_exception){ .value = (VALUE) })
Enter fullscreen mode Exit fullscreen mode

Then:

throw( new_int_exception( 42 ) );
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
rdentato profile image
Remo Dentato • Edited

Following your suggestion, I added a way to specify (and retrieve) additional information about exception.
A new object called exception allows access to this information
By default, expression_num, file_name, and line_num are available but you can specify others. The test/test7.c file has it explained.
As an example here is what it looks like:

#define expression_info  time_t time_stamp; int seq_cur;
#include "try.h"

  // somewhere in the the code:
  throw(ENOMEM, time(NULL), global_seq);

 // and in the `catch` code:
  printf("Seq: %d\n",exception.seq_cur);
  ... etc. ...
Enter fullscreen mode Exit fullscreen mode

Also, instead of positionally you can use the field names to specify just some of the information:

  throw(ENOMEM, .seq_cur = global_seq);
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
rdentato profile image
Remo Dentato • Edited

@pauljlucas. Also, now you can specify your own function to be called upon an unhandled exception:

#define tryabort() my_handler()
#include "try.h"

void my_handler()
{
  fprintf(stderr,"Unhandled exception %d @ %s:%d\n",
                 exception.exception_num, 
                 exception.file_name, 
                 exception.line_num);
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pauljlucas profile image
Paul J. Lucas

I don't quite get why you need the count member.

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

Since try is implemented as a for instruction, we need to count how many times we executed the loop's body (which is a chain of if/else). We only need to execute the body once, the second time we must exit the for loop.
Setting the count field to 2 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 the if with tryabort() will be executed.

It's a bit convoluted but if you follow the execution step by step should become clearer.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

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.

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

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.

Collapse
 
pauljlucas profile image
Paul J. Lucas

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 of setjmp() to a variable. You also don't take volatile into consideration.

Did you ever try building yours with -O2? I get incorrect results with my code even though it works fine with -O0.

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

The problem is local variables in the stack frame of the function are indeterminate:

void f() {
    int n_try = 0;
    try {
        ++n_try;
        throw( FOO );
    }
    catch( FOO ) {
        // ...
    }
    printf( "%d\n", n_try );
}
Enter fullscreen mode Exit fullscreen mode

That code does not guarantee that n_try will be 1 at the end. See here, this bullet:

  • except for the non-volatile local variables in the function containing the invocation of setjmp, whose values are indeterminate if they have been changed since the setjmp invocation.

The try macro calls setjmp(); throw calls longjmp(); setjmp() returns a second time. At this point, the value of n_try is indeterminate. On my platform compiling with -O2, the incremented value of n_try is lost since it was likely in a register. longjmp() does not restore such registers, so n_try is still 0.

The fix as alluded to is that you need to declare n_try to be volatile:

void f() {
    int volatile n_try = 0;
    // ...
Enter fullscreen mode Exit fullscreen mode

The problem is that you need to declare all local variables that are declared outside of the try block but modified inside the try block to be volatile. This is a very high price to pay. The programmer could easily forget to do so.

Using setjmp() and longjmp() can't really be used to implement a general exception-handling mechanism in C.

Collapse
 
rdentato profile image
Remo Dentato • Edited

@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 of setjmp() 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 them

If you look at the code now, there's just:

if (!setjmp(try_jb.jmp_buffer))
Enter fullscreen mode Exit fullscreen mode

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 the catch block I'll have to set them volatile or global. I agree that I should be more explicit about it, I only cited in passing in the test7.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.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas • Edited

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.

I guess we'll have to agree to disagree. It should be fine to write code like:

void f() {
    int n = 0;                 // missing `volatile`
    try {
        while ( some_condition ) {
            int x = g();
            n += x;
        }
    }
    catch( FOO ) {
        // ...
    }
    printf( "n = %d\n", n );  // unreliable!
}
Enter fullscreen mode Exit fullscreen mode

But if g() throws, the value of n is indeterminate. It might be the partial sum calculated so far (the useful result), or it could be 0. It's just too easy to forget declaring n as volatile.

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 calling break, continue, or return (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.

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

Yes, we agree that we disagree :)
In my mind, if g() throws an error, the value of n becomes (the vast majority of the times) irrelevant so it should be easy to spot those times when the catch block could use it for some recovery action and add volatile 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.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

FYI, my implementation is here. I added finally as well.

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

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 a finally 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 (see test8.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.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

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.

That's not the point. If you have:

void f() {
    FILE *f = fopen( "foo", "r" );
    try {
        // ...
        if ( condition )
            throw( FOO );
    }
    finally {
        fclose( f );
    }
}

void g() {
    try {
        f();
    }
    catch( FOO ) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

then f gets closed even if you throw in its try block.

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 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 to malloc & free it (unless perhaps it's possible to have the implementation free 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.

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 :(.

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.

I just happen to have put on GitHub my own implementation of a minimal test framework, I'd love to hear your comments.

It's definitely much less minimal than mine. Mine is about as simple as you can get and seems sufficient for my purposes.

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

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:

void f() {
    FILE *f = fopen( "foo", "r" );
    try {
        // ...
        if ( condition )
            throw( FOO );
    }
    catch() {
        fclose( f );
        rethrow(); // FOO will be raised to the parent
    }
}

void g() {
    try {
        f();
    }
    catch( FOO ) {
        // ...
    }
}

Enter fullscreen mode Exit fullscreen mode

Or even simpler:

void f() {
    FILE *f = fopen( "foo", "r" );
   ...
    if ( condition ) {
           fclose(f);
           throw( FOO );
    }
    ...
}

void g() {
    try {
        f();
    }
    catch( FOO ) {
        // ...
    }
}

Enter fullscreen mode Exit fullscreen mode

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 with finally 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.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Using finally, the file is closed even if no exception is thrown since you always want to close the file. Otherwise, you have to call fclose() twice: once at the end of the try block and in every catch block.

... if we could exit from the blocks with break, return etc.

You can exit from the blocks by using continue.

I look forward to seeing how you'll implement the extended fields of exceptions. I'm interested.

You might have to wait a while.

Collapse
 
seanlumly profile image
Sean Lumly • Edited

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.