Skip to content
simonlindholm edited this page Dec 7, 2014 · 10 revisions

Librcd includes its own implementation of exceptions, based on setjmp, and uses them frequently when it comes to threading and IO. Exceptions come with an exception type, a human-readable message, a backtrace, and sometimes other pieces of relevant information. The type can have one of a few pre-defined values (see rcd.h for more details):

  • exception_io: The general exception type. (E.g. syscalls, I/O, parsing syntax error, end of file)
  • exception_arg: Internal broken guarantees, non fatal release-mode assertion failures. You should generally never try to expect or "handle" this exception and avoid writing code so this exception needs to be thrown.
  • exception_join_race: Thrown when joining with a fiber that's either dead, or gets cancelled while joined with. You should never attempt to catch it directly. Catch exception_inner_join_fail instead.
  • exception_canceled: Thrown at a cancellation point when the current fiber is cancelled.
  • exception_fatal: An uncatchable exception; throwing one will immediately terminate the program.

The types are all powers of two - when catching exceptions you can bitwise-or several of them together to handle them in the same catch block. There are two predefined constants of this kind:

  • exception_desync: Same as exception_canceled | exception_inner_join_fail. It is usual to have an empty catch block for this within any fiber that is expected to be cancelled, which will absorb the cancellation and immediately return to indicate successful termination.
  • exception_any: For catching any type of exception (excluding exception_fatal). Use of this should generally be avoided.

The following code snippet examplifies the syntax:

try {
    throw("an error occurred", exception_io);
} finally {
    DBG("finally!");
} catch(exception_io | exception_arg, e) {
    DBGE(e);
}

The finally block executes before any catch blocks; apart from that, semantics generally match those of other languages. Braces after try, finally and catch are all optional, and finally and catch blocks may appear in any order.

To throw an exception, either the macro throw or throw_fwd (for chained exceptions) is used. This calls lwt_throw_new_exception, which will unwind the stack until it reaches a matching catch block, executing finallys and freeing subheaps along the way. The exception message will be copied and imported into the try block's heap. In case of joined fibers, the client fiber will receive the exception; the server will be unharmed. If no exception handler can be found, the whole program will be terminated (and not just the current fiber).

IO exceptions may optionally have a "class" - these are referred to as extended io exceptions (eios), and (depending on class) may carry additional data. Each class has an associated constant <class name>_eio, and in the case of complex 'eio's (i.e., ones with data) also a struct <class name>_eio_t. Here is a basic example, involving both simple and complex 'eio's:

define_eio(test);
typedef { fstr_t member; } test2_eio_t;
define_eio_complex(test2, member);
...
try {
    throw_eio("message", test);
} catch_eio(test, e) {
    DBG("caught exception: ", e->message);
}
...
try {
    emitosis(test2, data) {
        data.member = concs("errno is: ", i2fs(errno));
        throw_em("message", data);
    }
} catch_eio(test2, e, data) {
    DBG(data.member);
}

As seen, 'eio's are caught by using the catch_eio macro (or the catch-all catch(exception_io)), thrown by either throw_eio or emitosis + throw_em, and defined through define_eio or define_eio_complex. As a rule of thumb, throw eio exceptions for common errors within libraries, or for managing control flow, and catch them only when you are interested in recovering from one particular kind of failure out of many. Currently, librcd doesn't make full use of 'eio's; issue #7 is open for this.

To make threading more deterministic, exception_cancel exceptions will be thrown only at cancellation points. Those are: joining another fiber, blocking on IO, and calling lwt_yield or lwt_cancellation_point. Within an uninterruptible block, all cancellations - including within joined functions - are suppressed.

There is one quirk to be aware of: because the exception mechanism is based on setjmp, non-volatile local variables modified between try and throw are considered to have indeterminate values, and reading from them yields undefined behavior. (The reason for this is essentially that longjmp restores register state. See also section 7.13.2.1 of C99, and this Stack Overflow question.) Thus, any memory that need to be accessed from within finally, or after a catch, and that may have been changed during try, must either be volatile-qualified or allocated on the heap. In practice, this case should not come up all that often, because a thrown exception indicates an inconsistent state that should not be read from.

Clone this wiki locally