3
\$\begingroup\$

I've created simple try/catch macros that now I'd like to promote to wider use in my projects. I would have really liked to be able to do without global variables but I have not found any way to do it.

Any suggestion for improvement? Any issue that I may have overlooked?

    #include <stdlib.h>
    #include <setjmp.h>

    #define utl_trymax 16
    #ifdef UTL_MAIN
      int      utlErr = 0;
      int      utl_jbn = 0;
      jmp_buf  utl_jbv[utl_trymax];  
    #else 
      extern int      utlErr;
      extern int      utl_jbn;
      extern jmp_buf  utl_jbv[utl_trymax];  
    #endif

    #define utlTry      for (  utlErr = -1  \
                             ; utlErr == -1 && utl_jbn < utl_trymax \
                             ; (utl_jbn> 0 ? utl_jbn-- : 0 ) , \
                               ((utlErr > 0)? utlThrow(utlErr) : 0), \
                               (utlErr = 0) ) \
                           if ((utlErr = setjmp(utl_jbv[utl_jbn++])) == 0 )

    #define utlCatch(e)    else if ((utlErr == (e)) && ((utlErr = 0) == 0))

    #define utlCatchAny    else for ( ;utlErr > 0; utlErr = 0)

    #define utlThrow(e) (utlErr=e, (utl_jbn > 0 && utlErr? \
                                      longjmp(utl_jbv[utl_jbn-1], utlErr):\
                                      exit(utlErr)))

The macros are to be used in this way:

    utlTry {
      // Exceptions can be thrown here or in 
      // any function called from here.
      ... utlThrow(EX_OUT_OF_MEM);
    }
    utlCatch(EX_OUT_OF_MEM) {
      ...
    }
    utlCatch(EX_DB_UNAVAILABLE) {
      ...
    }
    utlCatchAny {
      ... //optionally catch all other exceptions
    }

if there's no handler for an exception, the program exits.

A simple example:

#include <stdio.h>

#define UTL_MAIN
#include "utltry.h"

void functhrow(int e) { utlThrow(e); }

int main(int argc, char *argv[])
{
  int k = 0;

  utlTry      { functhrow(2); }
  utlCatch(1) { k = 1; }
  utlCatch(2) { k = 2; }

  printf("Caught: %d%s\n",k,(k==2)?" (as expected)":""); 
}

Here is another version I wrote to avoid the problems of having to use global variables. The price is to have to pass an extra argument to try and throw. I'd like to have comments on the code. The syntax is much closer to the C++ one

    typedef struct utl_env_s { 
      jmp_buf jb;
      struct utl_env_s *prev;
      int err;
    } utl_env_s, *tryenv; 

    #define utl_lblx(x,l)  x##l
    #define utl_lbl(x,l)   utl_lblx(x,l)

    #define try(utl_env) \
                do { struct utl_env_s utl_cur_env; int utlErr; \
                     utl_cur_env.err = 0; \
                     utl_cur_env.prev = utl_env; \
                     utl_env = &utl_cur_env; \
                     for ( ; utl_cur_env.err >= 0 \
                           ; (utl_env = utl_cur_env.prev) \
                             ? utl_env->err = utl_cur_env.err : 0 ) \
                       if (utl_cur_env.err > 0) throw(utl_env,utl_cur_env.err); \
                       else if (!utl_env) break; \
                       else switch ((utl_cur_env.err = setjmp(utl_cur_env.jb))) {\
                              case 0 :

    #define catch(e)                   break; \
                              case e : utlErr = utl_cur_env.err; \
                                       utl_cur_env.err = -1; \

    #define catchall                   break; \
                             default : utlErr = utl_cur_env.err; \
                                       utl_cur_env.err = -1;  \

    #define tryend         } \
                   } while(0)

    #define throw(env,err) (env? longjmp(env->jb, err): exit(err))

I really dislike the tryend part. I could get rid of it by forcing C99. Not sure if that would be the right thing to do ...

\$\endgroup\$
  • 2
    \$\begingroup\$ Why not use C++ instead of building your own? \$\endgroup\$ – William Morris Apr 16 '13 at 19:57
  • \$\begingroup\$ @WilliamMorris I mostly do C programming. Even if I had the freedom to switch to C++, it wouldn't make much sense to do it just for having exceptions. On the other hand there are situation where try/catch simplfy the code a lot and I'm happy I can have them in C too. I'm just trying to be disciplined in what I use. \$\endgroup\$ – Remo.D Apr 16 '13 at 20:31
  • \$\begingroup\$ One problem with setjmp/longjmp is leaking resources - such as open file descriptors and allocated memory. Perhaps you need a 'finally' block as somewhere to put cleanup. You might also look at P99 (p99.gforge.inria.fr) which implements this type of behaviour and much else. \$\endgroup\$ – William Morris Apr 16 '13 at 22:32
  • \$\begingroup\$ Thanks, I wasn't aware of P99. They seem to use setjmp/longjmp as well even if in a more convoluted way. Their finally clause doesn't seem to be executed if one throws an exception within CATCH (but I'll check again). Anyway, have you spotted a place in my code where there's a resource leaking? Or you just meant that, if one is not careful, using try/catch might lead to forgetting releasing acquired resources? \$\endgroup\$ – Remo.D Apr 17 '13 at 4:51
  • \$\begingroup\$ Not sure what this gives you. The same can be achieved just by using return statements. The real advantage of exceptions is automatic resource management and for that you need RAII or a garbage collector in addition. \$\endgroup\$ – Martin York Apr 18 '13 at 13:11
4
\$\begingroup\$

One problem with this is resource leaks. It is not that your macros leak, but that when you use them, you'll leak. For example, suppose you call your utlTry in function a() that then calls function b() that allocates a resource (opens a file for example) and then calls function c() that does something with the resource. If c() fails and throws, the longjmp back to a() bypasses b() where the resource could be freed. There's no way round this problem and just 'being careful' is unlikely to be enough.

Another problem is that the code doesn't look or behave like normal code. It looks as if I can add a statement between the try and the catch lines, but clearly I cannot. And there is no indication that the catch blocks are conditional. Perhaps this is nitpicking - if it is just you using the macros, I guess you'll get along fine.

Talking of nitpicking, there are some small issues:

  • you mix naming styles, with camel-case utlErr etc and separate words in utl_jbn etc
  • utl_TRYMAX might be expected to be UTL_TRYMAX and if that limit is exceeded, utlTry seems to skip its contained block.

Overall, I'd say, don't do it. Error handling the long way can be a pain, but taking shortcuts like this is likely to be worse in the long run (especially if you share your code).

\$\endgroup\$
  • \$\begingroup\$ Thanks for the comments, William. I'll surely change the utl_TRYMAX it should have been utl_trymax. As for the utl_xxxx' vs utlAbcd', the convention is that first are for internal purpose only, while the lattere are supposed to be used by programmer. I should have said it in advance. \$\endgroup\$ – Remo.D Apr 17 '13 at 19:44
2
\$\begingroup\$

I'm sorry for my bluntness, but this code is absolutely horrible and unsafe. To worry about global variables while using a mess of function-like macros, together with setjmp/longjmp, is kind of like worrying about the paint of your car while smoke is raising from the engine and the breaks are dead.

Apart from that, trying to re-invent the C language is always a bad idea. Mainly because it confuses other C programmers.

You can implement exception handling in much safer and more readable ways:

typedef enum
{
  OK,
  ERR_THIS,
  ERR_THAT
  ...
} err_t;


err_t func (void)
{ 
  if(something)
    return ERR_THIS;

  if(something_else)
    return ERR_THAT;

  return OK;
}

int main(int argc, char *argv[])
{
  switch(func())
  {
    case OK:
      // code that should execute if no errors
      break;

    case ERR_THIS:
      // handle error
      break;

    case ERR_THAT;
      // handle error
      break;

     default:  // equal to C++ catch(...)
  }
}

And now you'll say "this is not exception handling!". Who cares. If you look at the generated machine code, this will generate exactly the same code as a C++ exception handling program. It will contain the same direct branches. The only difference is that the above program might contain less overhead and therefore execute quicker, with less memory usage.

\$\endgroup\$
  • \$\begingroup\$ Thanks for you comments, Lundin. Blunt feedback are exactly the reason I asked for a codereview. There would be little point otherwise :). That said, and just if you are interested, try to call some other function from func and assume there will be an error in that one. The point is that you need a way to propagate the error back to the first caller the cleanest way is to have all functions consitently returning an error code and build the rest around error code propagation. I confess I never managed to stay consistent with this style. \$\endgroup\$ – Remo.D Apr 18 '13 at 12:31
  • \$\begingroup\$ @Remo.D In a real scenario, you'd typically not want the errors to "fall all the way" back to the first caller. You'd probably want to catch 'em in the middle function, then pass them on. For example if the call stack is main -> add_to_queue -> linked_list_add_node, then the caller is not really interested in "Linked list exception, could not add node". They want a "queue full" error. \$\endgroup\$ – Lundin Apr 18 '13 at 19:54
  • \$\begingroup\$ I'm not saying that the "same error" has to propagate back. In your example, main is interested in the fact that an error has occurred and you need a way to convey that message. Trust me, I've seen plenty of real scenarios where errors went unnoticed, program exited without releasing resources and so on. Exceptions are just a tool, I'm not advocating their use everywhere. There are cases where they come handy. I just wanted to have comments on my code, it was not my intention to start a debate on error handling (which, I believe, don't belong to "Code Review" exchange). \$\endgroup\$ – Remo.D Apr 19 '13 at 7:05
  • \$\begingroup\$ I love this answer and this is the strategy I typically use with my php code to avoid exceptions. \$\endgroup\$ – W.K.S Apr 23 '13 at 23:35
  • \$\begingroup\$ Your statement "this will generate exactly the same code as a C++ exception handling program" is not remotely true. C++ generates code with no branching inside main, which is the key performance advantage of using exceptions - you only pay for them when they happen, unlike error codes which cost an extra branch at every call site. \$\endgroup\$ – Eric Nov 10 '18 at 1:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.