7. Error Handling Macros and Other Facilities¶
The header file error.h
is designed to support a particular style of error
handling. The general approach is that functions which can fail should return a
special error__t
return code, returning ERROR_OK
if successful, and
returning a code with contains a report of the failure reason if failed. A
sequence of such functions can then be combined with the ?:
operator so that
the overall result is success only if every stage succeeds. This can in some
sense be thought of as a more controlled variant of exception handling, or on
the other hand as combining success through the Maybe
monad.
Functions can be cascaded in this style if they conform to the following requirements:
ERROR_OK
is returned only on successful completion, otherwise all further processing can be aborted. Note thatERROR_OK
tests asfalse
when treated as a boolean.When any other error code is returned (testing
true
when treated as a boolean) the error code can be handed up, or reported there and then.Every error code must be subject to one of the following fates:
Passed up to the caller for further processing.
Explicitly reported using
error_report()
or a related function.Explicitly discarded using
error_discard()
.
In particular, values of type
error__t
must not be silently dropped as otherwise errors can go unreported and there will be a memory leak.
An example of this style of coding can be seen in the first half of the
implementation of ioc_main()
in examples/example_ioc/src/main.c
:
static error__t ioc_main(void)
{
return
initialise_epics_device() ?:
initialise_example_pvs() ?:
start_caRepeater() ?:
hook_pv_logging("db/access.acf", 10) ?:
load_persistent_state(
persistence_file, persistence_interval, false) ?:
TEST_IO(dbLoadDatabase("dbd/example_ioc.dbd", NULL, NULL)) ?:
TEST_IO(example_ioc_registerRecordDeviceDriver(pdbbase)) ?:
load_database("db/example_ioc.db") ?:
TEST_OK(iocInit() == 0);
}
The second half of this example uses the error handling macros. Not all
functions are of the form described above returning an error__t
error
code, so the macros in this file provide functions to convert functions of a
more familiar form into the format described here.
For example, the macro TEST_IO()
takes as argument an expression (which
may be an assignment) which following the usual system library calling
convention, that is, which returns -1 on failure and sets errno
,
otherwise returns some other value. The TEST_IO()
macro ensures that the
return code is tested and an error message is generated if necessary.
7.1. Core Handling Macros¶
The error handling macros defined here are of a uniform style. For each class
of test three macros are defined, for example for IO
we have the following:
TEST_IO()
, TEST_IO_()
, ASSERT_IO()
. These three patterns
behave thus:
TEST_xx(expr)
If the test fails an error code representing a canned error message (defined by the macro
ERROR_MESSAGE
) is returned, otherwiseERROR_OK
is returned.TEST_xx_(expr, format...)
If the test fails then an error code representing the given error message (with
sprintf()
formatting) is returned, otherwiseERROR_OK
.ASSERT_xx(expr)
If the test fails then an error report is printing showing the calling file name and line number, a traceback is printed if possible, and program execution is terminated. Execution does not continue from this point.
The following groups of tests are defined:
-
error__t
TEST_IO
(expr)¶ -
error__t
TEST_IO_
(expr, format…)¶ -
ASSERT_IO
(expr)¶ For these macros an error is reported when expr evaluates to -1, in which case it is assumed that
errno
has been set to a relevant error code, and it is reported as part of the error message.
-
error__t
TEST_OK
(expr)¶ -
error__t
TEST_OK_
(expr, format…)¶ -
ASSERT_OK
(expr)¶ These macros all treat expr as a boolean, reporting an error if the result is
false
. No extra error information is included in the error message.
-
error__t
TEST_OK_IO
(expr)¶ -
error__t
TEST_OK_IO_
(expr, format…)¶ -
ASSERT_OK_IO
(expr)¶ These all report an error if expr evaluates to
false
, and it is assumed thaterrno
has been set to a valid value which is used to report extra error information.
-
error__t
TEST_PTHREAD
(expr)¶ -
error__t
TEST_PTHREAD_
(expr, format…)¶ -
ASSERT_PTHREAD
(expr)¶ These macros are designed to be used with the
<pthread.h>
family of functions. These functions all return 0 on success and a non-zero error code which is compatible witherrno
on failure. Extra information from this error code is included in the returned result.
All of the TEST_
macros above return a value of the following type:
-
error__t
¶ This type encapsulates an error message or success, represented by the value
ERROR_OK
. Standard C error checking can be used to test for error or success: testingERROR_OK
behaves asfalse
, any error code representing failure tests as true.As noted above, error handling functions are designed to be chained with the
?:
syntax. Note also that error values must be handled witherror_report()
orerror_discard()
.
7.2. Auxilliary Error Handling Macros¶
The following macros are used as helpers.
-
ASSERT_FAIL
()¶ Functionally equivalent to
ASSERT_OK(false)
, unconditionally terminates execution and does not return.
-
error__t
FAIL
()¶ -
error__t
FAIL_
(message…)¶ Used to return a failure error code, functionally equivalent to
TEST_OK(false)
orTEST_OK_(false, message...)
.
-
error__t
DO
(action)¶ Used to convert a function returning
void
, or indeed any sequence of C statements, into a successful expression. Useful for including an unconditionally successful call in a sequence of error tests.
-
error__t
IF
(test, iftrue)¶ -
error__t
IF_ELSE
(test, iftrue, iffalse)¶ Conditional execution of tested functions. In both cases test is a boolean test; if it evaluates to
true
then the iftrue expression is evaluated, otherwise iffalse (if specified).
-
error__t
TRY_CATCH
(action, on_fail…)¶ This macro provides a limited form of exception handling. action must return an
error__t
, which is returned by this macro. If the error code is notERROR_OK
then the code on_fail is executed before returning. Note that any value returned by on_fail is discarded.
-
error__t
DO_FINALLY
(action, finally…)¶ This macro unconditionally executes finally after action has been evaluated, and returns the error code from action. Again, any value returned by finally is discarded.
The only difference from
TRY_CATCH
is that the finally code is unconditionally executed byDO_FINALLY
.
For the three multi-part macros IF_ELSE
, TRY_CATCH
and DO_FINALLY
,
the only separator between the key parts is a single comma character, so layout
and an extra comment should be used to structure these, as shown below:
IF_ELSE(test,
iftrue,
//else
iffalse) ?:
TRY_CATCH(
action,
//catch
on_fail) ?:
DO_FINALLY(
action,
//finally
finally);
7.3. Error Reporting and Management¶
The functions described here are used for reporting, discarding, or otherwise managing error codes.
-
bool
error_report
(error__t error)¶ Converts error into a string and uses
log_error()
to report the error. This is the normal destination for all error codes.true
is returned if error was an error (and an error message was reported), andfalse
is returned if error isERROR_OK
, in which case no action was taken.This function disposes of error, and this value is no longer valid.
-
bool
ERROR_REPORT
(error, format…)¶ This helper macro will augment error with the message defined by format before reporting the error by calling
error_report()
.
-
bool
error_discard
(error__t error)¶ This function silently discards error, after which the value is invalid. As for
error_report()
,true
is returned if error was an error, andfalse
if it wasERROR_OK
.
-
error__t
error_extend
(error__t error, const char *format, …)¶ If error is not
ERROR_OK
then the information associated with error is augmented with the message defined by format. The lifetime of error is unaffected, and the original error is also returned.This is useful for augmenting any error generated by expr as part of a processing chain. The format argument processing only occurs if expr returns an error. This is implemented as a macro to avoid unnecessary overhead in the absence of errors.
7.4. Message Logging Control¶
By default all error messages are sent to stderr
, but syslog can be used
instead.
-
void
vlog_message
(int priority, const char *format, va_list args)¶ Sends the given message to
stderr
or to syslog, depending on whetherstart_logging()
has been called.
-
void
log_message
(const char *message, ...)¶ -
void
log_error
(const char *message, ...)¶ Calls
vlog_message()
with with priority set toLOG_INFO
forlog_message()
, andLOG_ERR
forlog_error()
.Note that
error_report()
useslog_error()
.
-
void
start_logging
(const char *ident)¶ This invokes
openlog()
(3) and sends all future messages to the system log with the log identifier ident.
7.5. Miscellaneous Helpers¶
These macros have no other natural home and have found their place in this header file.
-
size_t
ARRAY_SIZE
(type array[])¶ If the number of elements of array is known at compile time this macro returns the number of elements.
-
to_type
CAST_FROM_TO
(from_type, to_type, value)¶ In some situations the compiler will not accept an ordinary C cast of the form
(type) value
because of anxieties about aliasing, or if aconst
attribute needs to be removed, or if some other low level bit preserving conversion is required. This macro performs this cast in a more compiler friendly manner (via aunion
type), and checks that value has type from_type and that to_type and value have the same size.For example, this macro is used to remove the
const
attribute from a hashtable key inhashtable.c
thus (hererelease_key()
takes avoid *
argument):static void release_key(struct hash_table *table, const void *key) { table->key_ops->release_key( CAST_FROM_TO(const void *, void *, key)); }
Another application is the following which extracts the bit pattern of a floating point number as an integer:
uint32_t bit_pattern = CAST_FROM_TO(float, uint32_t, 0.1F);
-
to_type
CAST_TO
(to_type, value)¶ This is a short-cut wrapper for
CAST_FROM_TO
for use in the case when from_type is no more specific thantypeof(value)
.
-
type
ENSURE_TYPE
(type, value)¶ This is a weak cast from value to type which ensures that it is valid to assign value to this type. Note that this will not work if type is a written out function type, in this case a typedef name would have to be used.
-
IGNORE
(expr)¶ Discards a return value without compiler warning even when
warn_unused_result
is in force.
-
unlikely
(expr)¶ Provides a hint to the compiler that
expr
is likely to be0
, can help in the optimisation of very rarely taken branches.
-
COMPILE_ASSERT
(expr)¶ -
STATIC_COMPILE_ASSERT
(expr)¶ This macro forces a compile time error if expr evaluates to
false
at compile time.COMPILE_ASSERT
must be used inside a function declaration, whileSTATIC_COMPILE_ASSERT
must be used at the top declaration level.
-
container_of
(ptr, type, member)¶ Casts a member of a structure out to the containing structure. For example, given py constructed thus:
struct xy { int x, y; } xy; int *py = &xy.y;
a pointer to xy can be reconstructed as:
struct xy *pxy = container_of(py, struct xy, y);
-
_WITH_ENTER_LEAVE
(enter, leave)¶ This macro is used to construct helper macros for wrapping enter and exit code around a block. Instances of this macro must be followed by a single statement or a block of code in braces, and the enter and leave clauses are used to bracket the code. In other words, the following use of this macro:
_WITH_ENTER_LEAVE(enter, leave) { statements; }
is exactly equivalent to:
{ enter; statements; leave; }
Note that the enter clause may include the declaration of a local variable, the scope of which includes the statements following and the leave clause.
Warning
break
andreturn
must not be used to exit the statement block guarded by_WITH_ENTER_LEAVE
or any of its derivatives. Usingbreak
will restart the block after reinvoking enter, and usingreturn
will bypass leave.-
WITH_MUTEX
(mutex)¶ This macro wraps the statement or block of code that follows with calls to
pthread_mutex_lock(&mutex)
andpthread_mutex_unlock(&mutex)
. The locking call is checked for success and an assertion failure is raised if an error is detected.Warning
Do not exit the guarded block with
break
orreturn
.-
WITH_MUTEX_UNCHECKED
(mutex)¶ Similar to
WITH_MUTEX
, except that lock errors are ignored.Warning
Do not exit the guarded block with
break
orreturn
.-
error__t
ERROR_WITH_MUTEX
(mutex, error)¶ This wraps mutex locking around an error expression, and evaluates to the result of evaluating error under the lock, which must be an expression of type
error__t
.
7.6. Pitfalls¶
The main pitfall is that if an error code is discarded then a memory leak will be created and errors will not be reported. Unfortunately it is very easy to do this by mistake.
Deliberately discarding the error code.
Example where error code is discarded, here we want to convert an
error__t
into a boolean indicating success:error__t test_function(void) { ... } bool bad_drop_error(void) { return !test_function(); }
In this case the error code is silently dropped. This should be rewritten in one of the following two forms:
bool noisy_drop_error(void) { return !error_report(test_function()); }
if the error should be reported, or:
bool quiet_drop_error(void) { return !error_discard(test_function()); }
if the error needs to be silently discarded.
Accidentially discarding the error code.
Inevitably there are many ways of doing this, but one way is particularly easy and unfortunate: consider this code:
error__t chained(void) { error__t error = function1() ?: function2(); function3() ?: function4(); return error; }
Oops. This code has two very unfortunate behaviours. Firstly, any error code returned by
function3()
orfunction4()
will be silently discarded … and worse, even iffunction1()
orfunction2()
fails, the last two functions will still be called.Do try not to do this. I think it’s impossible to persuade the compiler to pick this up, alas.
-
error__t
-
-