Back to libsm overviewlibsm : Assert and Abort
$Id: assert.html,v 1.6 2001/08/27 21:47:03 ca Exp $Introduction
This package contains abstractions for assertion checking and abnormal program termination.Synopsis
#include <sm/assert.h> /* ** abnormal program termination */ void sm_abort_at(char *filename, int lineno, char *msg); typedef void (*SM_ABORT_HANDLER)(char *filename, int lineno, char *msg); void sm_abort_sethandler(SM_ABORT_HANDLER); void sm_abort(char *fmt, ...) /* ** assertion checking */ SM_REQUIRE(expression) SM_ASSERT(expression) SM_ENSURE(expression) extern SM_DEBUG_T SmExpensiveRequire; extern SM_DEBUG_T SmExpensiveAssert; extern SM_DEBUG_T SmExpensiveEnsure; #if SM_CHECK_REQUIRE #if SM_CHECK_ASSERT #if SM_CHECK_ENSURE cc -DSM_CHECK_ALL=0 -DSM_CHECK_REQUIRE=1 ...Abnormal Program Termination
The functions sm_abort and sm_abort_at are used to report a logic bug and terminate the program. They can be invoked directly, and they are also used by the assertion checking macros.
There are three kinds of assertion:
is printed to stderr, and abort() is called. You can change this default behaviour using sm_abort_sethandler.foo.c:47: SM_REQUIRE(arg > 0) failed
By default, all three types of assertion are enabled. You can selectively disable individual assertion types by setting one or more of the following cpp macros to 0 before <sm/assert.h> is included for the first time:
SM_CHECK_REQUIREOr, you can define SM_CHECK_ALL as 0 to disable all assertion types, then selectively define one or more of SM_CHECK_REQUIRE, SM_CHECK_ENSURE or SM_CHECK_ASSERT as 1. For example, to disable all assertions except for SM_REQUIRE, you can use these C compiler flags:
SM_CHECK_ENSURE
SM_CHECK_ASSERT
-DSM_CHECK_ALL=0 -DSM_CHECK_REQUIRE=1After <sm/assert.h> is included, the macros SM_CHECK_REQUIRE, SM_CHECK_ENSURE and SM_CHECK_ASSERT are each set to either 0 or 1.
Sometimes an assertion check is significantly more expensive than one or two comparisons. In such cases, it is not uncommon for developers to comment out the assertion once the code is unit tested. Please don't do this: it makes it hard to turn the assertion check back on for the purposes of regression testing. What you should do instead is make the assertion check conditional on one of these predefined debug objects:
SmExpensiveRequireBy doing this, you bring the cost of the assertion checking code back down to a single comparison, unless expensive assertion checking has been explicitly enabled. By the way, the corresponding debug category names are
SmExpensiveAssert
SmExpensiveEnsure
sm_check_requireWhat activation level should you check for? Higher levels correspond to more expensive assertion checks. Here are some basic guidelines:
sm_check_assert
sm_check_ensure
level 1: < 10 basic C operations
level 2: < 100 basic C operations
level 3: < 1000 basic C operations
...
Here's a contrived example of both techniques:
void
w_munge(WIDGET *w)
{
SM_REQUIRE(w != NULL);
#if SM_CHECK_REQUIRE
/*
** We run this check at level 3 because we expect to check a few hundred
** table entries.
*/
if (sm_debug_active(&SmExpensiveRequire, 3))
{
int i;
for (i = 0; i < WIDGET_MAX; ++i)
{
if (w[i] == NULL)
sm_abort("w_munge: NULL entry %d in widget table", i);
}
}
#endif /* SM_CHECK_REQUIRE */
switch (foo)
{
...
default:
sm_abort("impossible value %d for foo", foo);
}
Note that I did not bother to guard the default clause of the switch
statement with #if SM_CHECK_ASSERT ... #endif, because there is
probably no performance gain to be had by disabling this particular check.
Avoid including code that has side effects inside of assert macros, or inside of SM_CHECK_* guards. You don't want the program to stop working if assertion checking is disabled.
First of all, to facilitate post-mortem analysis, we want to dump core on detecting a logic bug, disturbing the process image as little as possible before dumping core. We don't want to raise an exception and unwind the stack, executing cleanup code, before dumping core, because that would obliterate information we need to analyze the cause of the abort.
Second, it is a bad idea to raise an exception on an assertion failure because this places unacceptable restrictions on code that uses the assertion macros. The reason is this: the sendmail code must be written so that anywhere it is possible for an assertion to be raised, the code will catch the exception and clean up if necessary, restoring data structure invariants and freeing resources as required. If an assertion failure was signalled by raising an exception, then every time you added an assertion, you would need to check both the function containing the assertion and its callers to see if any exception handling code needed to be added to clean up properly on assertion failure. That is far too great a burden.
It is a bad idea to attempt cleanup upon detecting a logic bug for several reasons:
Here is a strategy for making sendmail fault tolerant. Sendmail is structured as a collection of processes. The "root" process does as little as possible, except spawn children to do all of the real work, monitor the children, and act as traffic cop. We use exceptions to signal expected but infrequent error conditions, so that the process encountering the exceptional condition can clean up and keep going. (Worker processes are intended to be long lived, in order to minimize forking and increase performance.) But when a bug is detected in a sendmail worker process, the worker process does minimal or no cleanup and then dies. A bug might be detected in several ways: the process might dereference a NULL pointer, receive a signal 11, core dump and die, or an assertion might fail, in which case the process commits suicide. Either way, the root process detects the death of the worker, logs the event, and spawns another worker.
We use the names SM_REQUIRE, SM_ASSERT
and SM_ENSURE in preference to to REQUIRE,
ASSERT and ENSURE because at least two other
open source libraries (libisc and libnana) define REQUIRE
and ENSURE macros, and many libraries define ASSERT.
We want to avoid name conflicts with other libraries.