Re: GException notes




Karl Nelson <kenelson@ece.ucdavis.edu> writes: 
> Then I don't understand how you intend to handle the memory at all.
> You are going to take a const format string and a set of arguments
> and print into some sort of char array.  Now unless that is a 
> static array for all exceptions, this means that that for some it
> must be duped.  
>

It's never a static array, it is always on the heap. See
gconf/gconf/gconf-error.c.
 
> What about languages wrappers which are going to return these
> exceptions potentially?  They will likely allocate resources
> differently and thus need a dtor to clean up their memory.
> 

class G_Exception {
  G_Exception(GException* e) : exception_(e) {}
  ~G_Exception() { g_exception_destroy(exception_); }
  G_Exception(const G_Exception& src) {
    if (exception_ == src.exception_)
      return;
    
    g_exception_destroy(exception_);
    exception_ = g_exception_copy(src.exception_);
  }

  string str() const { return string(exception_->str); }
  gint num() const { return exception_->num; }
}

// this following is trivial to machine-generate
void cpp_wrapper_for_func(gint blah, gint blah2)
{
  GException** exc = NULL;
  func(blah, blah2, &exc);
 
  if (exc != NULL)
     throw G_Exception(exc);
}

What's wrong with this wrapper? I know for certain the same approach
works in Guile, too. It would be nice to have various exception types 
based on the exc->num I guess, but you can trivially do that with a 
function to switch on exc->num and throw the proper exception.

For a global error object or stack you need this wrapper:

void cpp_wrapper_for_func(gint blah, gint blah2)
{
  clear_global_thing();
  func(blah, blah2, &exc);
 
  if (global_thing_not_clear())
    throw_exception_object_based_on_global_thing();
}

i.e. your wrapper is exactly the same number of lines of code, if 
nothing else.

> The other key advantage to having a dtor is that the exception
> can be derived if need be.  (Thus allowing bindings freedom to
> pass additional info through if they need to.)
> 

I do see the need for a destroy notify on user data for the exception,
if we add a user data field.

None of the other boxed types have this generic destructor so I am
reluctant to say GException is different. Should all boxed types have
a gpointer extra_binding_data with a destroy notify function?
  
> > Requiring return codes is somewhat gross; it often leads to an
> > unnatural API (for example gconf_get_bool() couldn't return a bool).
>
> I can agree with this.  However, I don't see the requirement as
> being there.  The exception checking is outside the error code.
> We are really just talking about extra return info just like I
> did in libsigc++ for the return code ignore.
>

If there's no return code then you have to have an indication of fatal
error (non)occurrence in the exception itself. How do you intend to
indicate this?
  
One of the big problems with return codes is that they are
inconsistent (NULL, boolean, -1) and thus annoying to handle in
language bindings. :-)
 
> Well, you connect signals to functions.  If you add exceptions to 
> functions you change their profile and thus make those impossible to
> connect, even when the user doesn't care about the exception code
> or they want the exception farther up in the chain.  It adds
> the additional complexity that signals man have some connections
> with exceptions and some which do not.  
>

You do not connect any old function to a signal. You connect a
function with the proper signature.

No signal right now has an error object in the signature, and none
ever should, because it makes a mess that doesn't make sense.  It's
impossible to handle errors for an arbitrary set of user callbacks.
It's like having a Java method that throws every possible exception.

You can't use a function that raises an exception as a signal handler
because you have to check the error code on a function that raises an
exception, and there is no way to run code to do that in between
signal handlers.

So you need a wrapper to check your error code. The simplest way to
check the error code is to always ignore it:
 
 void my_callback(object, blah blah)
  { 
     raises_exception(blah blah, NULL);
  }

already you often need such a wrapper, except for the occasional
coincidence when a stock function happens to "match".

> (Of course I am not really sure where the exception code is really
> being used.  
>

Typically for a small well-defined function, like
 gconf_get("/some/setting", &exc)
or
 pango_convert_string(str, &exc)
or
 gdk_pixbuf_load_file(filename, &exc)

where you have a limited set of errors that are immediately
interesting if they are interesting at all.

If you do any of these things in callbacks, the callback will need to
immediately handle it, and that only makes sense. Some unrelated code
section that happened to result in a signal should not handle it.
 
> > To me it's very unclear what is happening in this code. I can guess
> > what you mean, but have no real idea. It also looks like you have to
> > write a separate error handling routine. Compare to:
> 
> Well, we could go with the really evil macro magic...
> 

Evil macros make it more unclear.

If you have language support, then you can have everything Just
Work. If you don't, then it doesn't, so you need to know what is
really happening.

> > Here it is very transparent what's going on. To me this is a huge
> > benefit.
> 
> I think transparency is a very bad thing in a system.  Then the
> user will come to depend on the particulars of the implementation
> and thus ignore the interface.  (This is my biggest grip with 
> Glibs list.)  Transparency should not be a goal!
>

I mean transparency of logic and control flow. 

My code clearly says: 
 - start out with a clear error
 - call a function that definitely by its signature could cause an
   error
 - if an error occurred take action

This is a separate issue from abstract data types. If you wanted to
hide exception->str and exception->num behind accessor functions then
that's another issue.

Your code has exatly the same logic and control flow but it is not
apparent on the surface:

  try(); /* what does try do? it doesn't define a block as in C++ */
  frobate(); /* are we sure frobate() can raise exceptions at all? */
  catch();  /* does this return from the function or what? */

renaming these things helps a lot though. :-)

> > An alternative approach is to set a global error object instead of
> > ignoring errors whenever the user passes NULL for the GException**
> > argument. Then you can basically have both APIs. I'm not sure if the
> > functionality duplication is worth it.
> 
> This still leaves the problem of do we make a system where the
> signal system is dealing with exceptions.   They are very 
> problematic to add arguments.  
>

I can't think of a single case where a signal emission should raise an
exception. A signal emission is supposed to _notify_ you of a
change. How can you fail to notify someone of a change? There is no
error that makes any sense.

If a signal emission has an error, it means one of the _handlers_
failed. But _by definition_ the signal emitter doesn't know anything
about the handlers, that is the _whole point of signals_. It makes
zero sense for the signal emitter to handle these errors; the handlers
should be handling their own errors.

I would be interested to here a concrete example of this
signal-emission-causes-error stuff. Otherwise it is just a theoretical
concern overcomplicating the real-world cases.
 
> True, but it would absolutely kill the gtk-- language binding.  
> We have made the explicit assumption that every argument has a
> corresponding C++ type.  Adding optional arguments means there is
> no correspondence.  (Okay so this is my own personal headache, but
> I think that others will find it similar.)
>

Oh come on, you just add a new m4 macro for the has-exception
case.

I'm confident that all the binding authors have the skills to machine
generate the cpp_wrapper_for_func() example I gave earlier.
  
I know for a fact that this is easy in at least the Guile and Sugar
cases because I've tried them, and I strongly suspect it works easily
in gnome-python as well, and I think the code at the top of this mail
would work fine for Gtk-- in conjunction with a new m4 macro.

> The problem with adding exceptions as such is that we are heading down
> the road of admitting that C isn't good enough.  Exceptions, typesystems,
> signals are all components provided by java, C++, ObjC.  It feels a
> lot like reinventing the wheel here.  Exceptions are something 
> best left to the compiler.
> 

The UNIX API is a good example that you can easily do error reporting
in C. Works great, there are hundreds of thousands of apps using it.

I actually hate most language exception support such as Python because
it's far too easy to forget to handle exceptions and crash your whole
app. Java does this right by requiring you to explicitly handle or
throw all exceptions.

So: perhaps we shouldn't call it GException. We are not trying to do
some kooky exception emulation. We are just trying to report error
information, as the UNIX API does.

The primary reasons for a GError object rather than UNIX-style is that
we want better messages than strerror(), we want extensibility (new
error types, basically unlimited errno values), and we want thread
safety.

Let's not think of this as an attempt to do exceptions in C.

Complexity is death here, I promise.  This is especially important
because error-handling codepaths are rarely tested.

KISS. You want to a) know the operation failed b) have a useful error
message and c) be able to programmatically respond to certain kinds of
error. It has to be threadsafe. Any other features are just complexity IMO.

Bad features:
 - exceptions that are just warnings. There's nothing to do with these 
   except spew them to stdout or a dialog. If users really care 
   they should be fatal, not warnings. If users don't care you 
   shouldn't spew them. ergo they are useless.

 - flexibility is fairly bad. You need conventions that are enforced 
   as much as possible, or people break the invariants that keep the
   system working. For example they try to make signals have 
   exceptions or they allow exception-pileup that creates 
   post-functional ambiguous fatality. ;-)
   Excessive flexibility also increases the amount of special cases 
   after every error check.

   The following should always work:
     if (error_occurred)
        popup_error_dialog(get_error_string());

   if you have to do crazy switches and checks to get around to the
   dialog then the API is too annoying.   

   Flexibility here also leads to inconsistent use of the API and thus 
   people make mistakes and miss errors. 

   The GException argument approach, and I think also a simple global
   error object that has to be cleared before each error-causing
   function, are sufficiently flexible for all real-world cases. It
   works for the entire UNIX API for example, and for CORBA.      

   If you are planning to be more complex than either of those then 
   you have me really scared as hell. :-)
 
Havoc



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