GException notes




Hi,

This is some stuff Owen and I were talking about, I wrote it down a
while ago and added some details tonight.

Havoc


What information an exception contains
===

You want to move the following information from a function that can
fail to the calling function:

  - whether or not a fatal error actually occurred. 

    In the Unix API, this is generally indicated by a return value.
    The state of errno is explicitly NOT a reliable indicator of this;
    errno can be set as an irrelevant side effect and the library
    functions do not reset it to 0 if the function succeeds.

    However there are cases where errno must be used, such as
    strtoul(), where users are required to set errno to 0 before
    strtoul() is entered and then check errno != 0 after strtoul() in
    order to detect errors.

    In CORBA, the CORBA_Environment object reliably contains an error 
    if an only if a fatal exception occurred; this is equivalent to
    all Unix API functions setting errno to 0 on entry and never 
    setting an error unless a fatal error actually happened. That is,
    it makes the return code redundant.  

  - a program-understandable code representing the error, so 
    error handling can be special-cased by type. 
  
    For example in the Unix API an errno is returned and you can
    switch (errno) to handle different errors in different ways.

    In languages that support exceptions you can generally handle
    either specific exceptions or "any exception." For example in 
    C++ you can say catch (MySpecialException e) or catch (...), 
    and in Python you can say except Blah: or just except:.

  - a human-readable string describing the error. 

    This can either be a strerror() on the error code, or an
    on-the-fly constructed string containing more information.
    The latter probably delivers more information to the user,
    such as the name of the file which wasn't found, or details
    on the context surrounding a parse error. This string 
    should be internationalized.   

What types of errors are exceptions?
===

Exceptions should be used when it would be correct for the calling
function to handle an error, rather than avoiding it.

That is, if NULL is an invalid argument to foo(), and I call the
function:

 foo(NULL);

then this code is nonsense:

try {
  foo(NULL);
} 
catch (BadArgException) {
  /* oops, shouldn't have passed NULL */
}

BadArgException should not exist, because user code should be:

 if (blah != NULL)
   foo(blah);

i.e. handling BadArgException always indicates broken user
code. Exceptions should indicate exceptional situations stemming from
external-to-the-program circumstances, such as data read in from files
or from across the network.

The glib/gtk convention is that programmer errors like passing NULL to
foo() should be caught with g_return_if_fail() checks, not by throwing
exceptions.


Possible conventions for function callers in C
===

A "function caller" is some code that invokes a subroutine that can
throw an exception.

One convention is to indicate the presence of an error by return code, 
or not:

 if (!foo()) 
   error
 
 if (foo() < 0)
   error

The alternative convention here is to indicate the presence of an
error by setting an error object:

 errno = 0;
 strtoul();
 if (errno != 0) 
   error

 CORBA_blah_blah(&ev);     /* CORBA stubs zero the exception object
                              before the implementation of blah_blah
                              is entered */
 if (ev._major != CORBA_NO_EXCEPTION)
   error


 GConfError* error = NULL; /* manual exception-object-zeroing is required */
 gconf_set(..., &error);
 if (error != NULL)
    error

 Note that you could also have this convention:

  GConfError* error;
  gconf_set(..., &error); /* gconf_set includes "error = NULL" */
  if (error != NULL)
     error
   
Possible convention for error-causing functions in C
===

These basically mirror the conventions for callers.

If the return code reliably indicates success/failure then you can set
the error even if there is no fatal failure. This works well with a
global error object like errno, since the error will be freed anytime
a new error is set; with a CORBA_Environment- or GConfError-style
object, however, you end up leaking RAM this way unless you impose the
following on users:

 if (!foo(&err))
   error

 if (err != NULL)
   error_free(err);
 
i.e. users always have to free the error object even if no error
occurred.

Therefore it makes sense to use the presence or nonpresence of an
error object reliably indicate syccess/failure.  If you don't impose
that burden then err != NULL must be a reliable indicator of whether
an error occurred. This means that "superfluous" errors can't be set;
all errors must be either handled or passed upward as a fatal error.

For example, given the functions foo() and bar() that can both fail,
the following code is broken:

  foo();
  bar();

because an error caused by foo() can be left set, even if bar()
succeeds and the routine as a whole succeeds. After foo() the code
should either return, or handle the error and unset it.

The code should be:

 foo();
 handle_error(); /* or alternatively return immediately */
 bar();
 handle_error(); /* or return */

It is also important to define whether the called function or the
function caller must clear the error object when entering the
subroutine, as described earlier. The object must be cleared either by
the caller (as in GConf and strtoul()) or by the callee (can't think
of an example) or by the underlying infrastructure (CORBA stubs).

setjmp()/longjmp()
===

Some people have proposed implementing exceptions in C with
setjmp()/longjmp(). This approach is not considered acceptable for
glib; among other things, it is more or less guaranteed to leak memory
without destructors/GC and other language support. It is also
cumbersome to use and interoperates poorly with existing C libraries
and with language bindings.

Concrete proposal
===

My concrete proposal for GException would be to basically copy the
GConf/Pango system which seems to work well in practice.

glib would require an additional feature, namely the ability to
distinguish between exceptions created by various modules.

struct _GException {
  GQuark module; /* some sort of identifier for each module, 
                    still unclear how this works */
  gint num;      /* errno-style code, defined as an enum in 
                    each module */
  gchar *str;    /* human-readable description string */
}

To get an idea of the API, look at gconf/gconf/gconf-error.[hc] and
some of the uses of it throughout GConf. Some highlights:

 - functions that raise exceptions take GException** as their final
   argument. If NULL is passed for this argument, then the caller
   wants to ignore errors and no errors are reported. If a valid 
   pointer is passed, it should point to a GException* initialized
   to NULL. 

 - If and only if a fatal error occurs, an exception is placed in this
   GException** location

 - Caller must check whether the error was set, and free the
   GException if so (unless NULL was passed to ignore errors)

Sample application usage:

  GException *exc = NULL;

  frobate(blah, blah, blah, &exc);

  if (exc != NULL) 
    {
       fprintf(stderr, _("Oops: %s"), exc->str); /* or switch (exc->num) */
       g_exception_destroy(exc);
       exc = NULL;
    }

or to ignore errors, if you are Evil:
 frobate(blah, blah, blah, NULL);

Sample function that causes an error:

void 
frobate(gint blah, gint blah2, gint blah3, GException **exc)
{

  if (!do_stuff()) 
    {
      /* g_exception_set is like gconf_set_error() and properly 
         handles exc == NULL */
      g_exception_set(exc, _("Error blah blah with stuff %s"), blah2);
      return;
    }

  if (!do_other_stuff(exc))
      return; /* pass up the exception */
}

Note that if frobate() were more polite and had no other meaningful
return value, it would likely return a boolean (TRUE = success, FALSE
= failure) for the application programmer's convenience. Note that
TRUE for success is the established convention and should be used.

Making an earlier point concrete, the following code is broken:

  void
  frobate(gint blah, GException** exc)
  {
    do_stuff(exc);
    do_other_stuff(exc);
  } 

The reason is that do_stuff() failing is either fatal or it isn't. If
it isn't, and it fails, and do_other_stuff() succeeds, then you have
improperly signaled that an error occurred (exc should have been
cleared after calling do_stuff()). If it is fatal, then you have to
check for error right after do_stuff() and probably return
immediately.

The rule is: either handle and clear the error immediately, or return
from your routine immediately; there are no other options. If you are
main(), you either have to handle it or exit the program. This is how
all languages with exceptions behave. Passing NULL for the exception
object is an implicit "handle all errors by ignoring them" as in catch
(...) {/*nothing*/;} or except: pass. You can't say "leave this
exception sitting around and raise it if no other exception gets
raised."

Outstanding issues
===

 - Should GException have a reference count or a copy function

 - [your issue here :-) ]


Havoc



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]