Hey Didier,
> ??? Yes the exceptional path is important and it is easily treated
> apart, and it should be. The problem is only that the propagation (not
> the treatment) of the exceptions corrupts the normal path all allong up
> the call chain until the place where they will be treated. It reserves
> a
> space in the range of the return values of all functions and forces the
> callers to assert it. Just this bare propagation is one half of the
> source code! It would be fine if it was separate, but it's interleaved
> by design.
I think the trick is to make the error handling code as minimal as
possible so it does not disrupt the readability of the ordinary flow.
Ideally it would be a one-liner such as a goto or a call to a function.
In some cases that function can simply clean up and abort. In cases
where the error handler must clean up and then resume at a specific
point, you could aim for a 2-line error handling block.
> ??? I'm thinking more and more that, with my current development in C,
> I need to experiment setjmp/longjmp for the first time. Otherwise it's
> so ugly! I'm dealing with exceptional conditions detected by the
> program
> itself, not zero-divide or the like happening by surprise -- these have
> total legitimity to crash my program. I actually deal with just two
> exceptions: Protocol_Error and Premature_End_Of_Data. These exceptions
> can be detected by several functions, at various levels but must be
> treated in just one place, near the top level of the application.
As something of a C expert I would definitely advise against using
setjmp() / longjmp(). Please see my earlier comments on the topic. This
should be considered a legacy feature. For technical reasons, as I
explained earlier, the compiler cannot produce good code for a function
that calls setjmp(). So either you will have sub-optimal code, or else
register variables may be corrupted during the jump.
I would like to propose to you a solution from some code that I wrote
recently. I took a real function, which runs in a microcontroller and
its purpose is to receive blocks of data through a serial link and write
them to the microcontroller's flash. I converted most of the function to
pseudocode, so you can only see the bare outline where it receives
frames, does something with them and handles errors. Sorry for the
verbosity, but it was necessary to provide a non-trivial example (some
of my real life examples are a lot more complicated...).
uint16_t system_loader_flash_write(struct msg_buf *frame) {
uint16_t status;
if (... incoming message at "frame" does not make sense ...) {
status = SYSTEM_STATUS_COMMAND_ERROR;
goto error;
}
// workaround for erratum 2.14.1 NVM Read Corruption
... put the chip into a specific mode ...
for (... loop to receive messages ...) {
struct msg_buf *p; // pointer to incoming message
status = system_rx_command(..., &p); // try to receive a message
if (status != SYSTEM_STATUS_SUCCESS)
goto error1;
for (... loop through contents of message at "p"...) {
if (... writer routine returns false indicating an error ...) {
msg_buf_free(p);
status = SYSTEM_STATUS_NVM_ERROR;
goto error1;
}
... do something ...
}
msg_buf_free(p);
}
status = SYSTEM_STATUS_SUCCESS;
error1:
// workaround for erratum 2.14.1 NVM Read Corruption
... put the chip back into normal mode ...
error:
msg_buf_reset(frame, NULL, 0);
return status;
}
In this code, "frame" is an in/out parameter and all paths must clear
the frame before returning. Thus at a minimum, execution must pass
through the "error" label. And also, if the initial parameter validation
passes, the chip will be placed in a special mode and once this is done,
execution must pass through the "error1" label to clean up. Finally, if
a problem happens during the execution of the loop, the message buffer
at "p" must be freed before going to error1. If this was occurring in
multiple places I would have defined a label called "error2" within the
loop which would call msg_buf_free(p) and then goto error1, but it was
not necessary to do so in this example.
Note: In this case the "good" return can happen via the error handling
code, but in other similar routines this is not possible and so there
are 2 versions of the cleanup-and-return procedure, one for success and
one for error (which is placed after the success return).
What do you think of the approach generally? Error handling is separated
out and placed towards the end of the function and does not majorly
disrupt the flow of the algorithm. Errors are propagated where
appropriate (see how the call to system_rx_command returns a status
which is passed upwards to the caller after doing cleanup steps). Error
handling code is only used where it makes sense (see how the writer
routine does not bother with returning a numeric status and instead just
returns true for success, false for failure).
kind regards,
Nick