Fog Creek Software
g
Discussion Board




Elegant error handling in C APIs?

Joels article on exception handling got me thinking—about C.

When you don't even have exception handling to begin with, how do you prefer to handle errors? 0/-1/NULL return values and error codes/error strings, or 0/-1/NULL return values and error callbacks, or something else (longjumps, goto, etc.)?

Can you point to any examples of C APIs you feel get it "right," or at least as close to "right" as one could hope to get?

Justin++
Friday, March 19, 2004

I like the way that the Symbian OS handles errors.  Symbian is written in C++ but they started development before exceptions were standardized.  So they basically invented their own exception scheme and error return codes.  There is really no reason this scheme couldn't be used in C.  In fact, there are several C exception handling projects:

http://www.on-time.com/index.html?page=ddj0011.htm
http://cexcept.sourceforge.net/

The basic idea is that you use the setjmp and longjmp standard C functions to set and restore the stack frame.  You can return a single integer error code or perhaps use it as pointer to an error structure.  When an error occurs you restore the stack and your "exception handler" is called.

The second part is that you have a "cleanup stack" where you push pointers to allocated heap memory.  This is so that when your exception is raised all the heap memory you've locally allocated is also removed.  (In Symbian, being it's C++, this also calls your objects' destructors)

You end up with code that looks something like this:

void main() {
  TRY {
    SomeFunction();
  } CATCH(e) {
    if (e == ERR_NOMEMORY) {
      /* Display error */
    }
  }
}

void SomeFunction() {
  void* mem1 = malloc(100000);
  if (!mem1) RAISE(ERR_NOMEMORY);
  cleanup_push(mem1);
  void* mem2 = malloc(100000);
  if (!mem2) RAISE(ERR_NOMEMORY);
  cleanup_push(mem2)

  /* Do some work on mem1, mem2 */
 
  cleanup_pop(mem2);
  free(mem2);
  cleanup_popandfree(mem1);
}

If the first memory allocation fails then transfer is returned (via longjmp()) to the CATCH and the error code is handled.  If the second memory allocation fails then the first memory allocation is freed and control is returned to the CATCH.

I'm not sure I've explained this all that well -- but I definately like this system and would probably implement it in a C project.  From my experience, it works exceedingly well.

Almost Anonymous
Friday, March 19, 2004

My answer to elegant error handling in a language without exceptions is to implement exceptions in the language :).

A summary of error-handling techniques:
http://www.freetype.org/david/reliable-c.html

I think experience has shown that non-local exceptions with automatic clean-up of stack objects is the "right" way to handle errors in C-like languages.

(note that "automatic clean-up of stack objects" is the most important part about this; exceptions hardly make sense without automatic stack clean-up.)

It is possible to implement such a scheme in plain C using setjmp/longjmp and a stack of "cleanup" functions. The impact on most source code is that you'll have to call a macro at the entry and exit points for each function that uses stack-allocated objects (to push the cleanup stack location on entry and pop it on exit, so that exceptions nest properly).

Dan Maas
Friday, March 19, 2004

stack cleanup.. you mean heap tempories, right?

i like i
Friday, March 19, 2004

I mean things like:
{
File foo = open_file(filename);
...
}
where "foo" is automatically closed when the block is exited (either by normal function return or by an exception), without you having to manually check for errors and close the file.

C++ works this way by default (if you use the resource-acquisition-is-initialization pattern) but C requires some implementation effort to get the behavior. (open_file() must internally push its cleanup function onto a stack, and there must be a way to unwind the cleanup function stack on block exit)

Dan Maas
Friday, March 19, 2004

The way I've always done it is to use good ol' fashioned
goto to implement exceptions, ie

int foo(int x)

{
    char *str = NULL;

    ...some processing...

    if (shit_happens) goto error;

    ...str gets set to something, using malloc()...

    if (disaster) goto error;

    return(SUCCESS);

error:

    if (str != NULL) free(str);
    return(FAIL);
}

If you adopt this approach, it is good to use it as a
style for unwinding.  setjmp/longjmp can work too,
although you have to have a solution for memory cleanup,
ie a stack of memory allocations that is blown away if
you have a failure  and is cleaned up if whatever
structure you're building is successfully created.

x
Friday, March 19, 2004

> Symbian is written in C++ but they started development before exceptions were standardized.  So they basically invented their own exception scheme and error return codes

I remember that MFC had an exception handling mechanism based on longjmp (before they added C++ exceptions to compiler/runtime).


----
now with C/C++ api's there are different valid ways to pass errors.

- return 0 for success error code otherwise
int fooo(); 

- return -1 on error (set errno or SetLastError with extended error indication).
int fooo()

-- return true/false (set errno or SetLastError)
bool fooo()

-- bool foo(    , int *ErrorCode)

-- bool foo() // an store error code in class member

-- void foo() // and throw exception.

The fun part comes when the project mixes different styles, so the caller of a function can err in calling and handling errors, so i guess this matter can be solved by enforcing some rigid coding convention.
(Funny that most coding conventions care about some other stuff, usually with how you put the braces and how many spaces/tabs there are. not what really matters.
rant rant rant)


;

Michael Moser
Saturday, March 20, 2004

*  Recent Topics

*  Fog Creek Home