14.4 Additional utilities provided by fsc2
When writing a module some of the following information may be useful:
first a special function for printing out messages to the user is
discussed, second a function that simulates usleep()
but does not
share some of its shortcomings.
The third topic, exceptions, is a well-known concept implemented for
example in C++
. Unfortunately, C
does not have this kind
of mechanism, but when being a bit careful one can implement something
very similar also in C
using a few macros.
When writing the program I had to deal with my share of memory leaks, segmentation faults etc. and hacked together a few routines for allocation and deallocation of memory that have some build in code to help me with debugging (and which throw exceptions when an allocation fails). You may find it useful to also use these routines for your modules.
A boolean type is something that was missing until the new C99
standard (which only a few compilers support yet), so there's already a
typedef
for this type included in fsc2
that you can
use. It's also documented here in order to avoid confusion should you
accidentally try to redefine it.
Finally, there exist some utility function for rounding of double values to the different integer types as well as converting integers to types of smaller width.
14.4.1 Printing out messages
When writing a module one often has to print out messages to inform the
user e.g. about invalid arguments etc. For this purpose there's the
print()
function in fsc2
(not to be mistaken for the
built-in EDL
function with the same name) that helps to deal
with this and prints messages to the lower browser in the main
form. Except for the first argument the function is identical to the
printf()
function in C
, i.e. the second parameter is a
format string of exactly the same format as printf()
expects,
followed by as many values as there are conversion specifiers in the
format string.
The first parameter is an integer describing the severity of the problem. There are four levels:
-
NO_ERROR
Just an informational message (in black) -
WARN
A warning message (in green) -
SEVERE
A severe warning, which the user really should think about (printed in blue) -
FATAL
A fatal error message (printed in red) - to stay consistent with the usual way this type of error message is used you should now throw an exception (see next subsection) to make the program stop.
To the output the EDL
file name and line number (if
appropriate) as well as the device and function name is prepended.
The full C
declaration of this function is:
void print( int severity, const char * fmt, ... ); |
14.4.2 Determining the time
To find out how much time has been spent since the start of the
experiment the function experiment_time()
may be used. It returns
the time in seconds since the start of the experiment (to be precise
since the start of the first exp_hook function). The time resolution
should not be taken to be better than about 10 ms.
The function also can be used during the test run but in this case only a very rough estimate will be returned that easily could be off by more than an order of magnitude.
The full C
declaration of this function is:
double experiment_time( void ); |
14.4.3 Waiting for short times
When writing code that deals with real devices one often needs to wait
for times with a resolution of less than a second. The usual way to do
this is to call either usleep()
or nanosleep()
. The
second function, nanosleep()
, may look like a bit of overkill
since both functions effective time resolution is usually in the ms
range, at least on Intel machines. On the other hand, usleep()
is marked as obsolete in the IEEE Standard 1003.1-2001 (Single UNIX
Specification, Version 3) for several reasons (mainly for its
undefined interaction with the SIGALRM
signal and other similar
functions) and thus should not be used anymore.
As a replacement for usleep()
(which you shouldn't use in a
module) there is a function
int fsc2_usleep( unsigned long us_duration, bool quit_on_signal ); |
It takes two parameters, an unsigned long
, which is the
duration (in milliseconds) to wait (just like usleep()
), and a
boolean value that indicates if the function is supposed to return
immediately if a signal gets caught or if it should wait for the
specified time even on signals. The function is, except its dealing
with signals, nothing more than a wrapper around nanosleep()
,
so you can use nanosleep()
yourself if you prefer.
14.4.4 Assertions
At least during development it usually is helpful to also test for
conditions that should never happen but may anyway due to bugs. In
this case often the standard C macro assert()
is used.
fsc2
comes with two variants of this macro,
fsc2_assert()
and fsc2_impossible()
. The first one is
basically identical to assert()
, the only difference being that
in case the assertions tests true not only a text, showing the the
file and line number, gets printed to the standard error channel but
also an email is sent (including the text of the currently running
EDL
script and some more relevant information) to whatever
email address has been set during the installation as the one for
receiving such mails about crashes and other fatal states of the
program. The macro fsc2_impossible()
is for the same purpose
but doesn't take an expression as its argument, it is meant for
situations in the code where one would end up in in a branch that (at
least theoretically) can never be reached - e.g. if you test in a
switch all possible cases you could add a default:
case to
catch a situation where you actually forgot about a possible case.
If, as in the case of the assert()
macro, the macro NDEBUG
is defined (before you include `fsc2_module.h') both functions don't
do anything at all. You don't have to worry about cases where one of
these macros is used as the only statement following for example an
else
like in
if ( x == 1 ) do_something_1( ); else if ( x == 2 ) do_something_2( ); else fsc2_impossible( ); |
The macros are written in a way that they form an (empty) block also when
NDEBUG
is defined.
The only thing you've got to worry about with fsc2_assert()
is the
same thing you have to with the assert()
macro: if the expression
has side effects then the programs behaviour may differ depending on
whether NDBUG
is defined and you thus may have intoduced a Heisenbug.
14.4.5 Error handling with exceptions
One of the most important but also most tedious things in programming is rigorous error handling. In order to make a program failsafe in every place where there is even the remotest chance something may go wrong one has to include error handling code everywhere. This is especially annoying when an error can happen with deeply nested function calls where the type of the error may have to "bubble up" several levels before it is finally can be dealt with.
In order to alleviate this problem several newer programming languages
like C++
introduced a concept called "exceptions". Unfortunately,
C
doesn't have them but in fsc2
I use a method to "fake"
exceptions(26). An exception can be seen
as a kind of flag that can be raised at any instance in the program and
leads to the flow of control being immediately transfered to a place were
the error can be dealt with (another way to see it is as a non-local
goto
which knows all by itself were to go to).
There's one restriction with fsc2
's exceptions: you may not
use them from within code that might get called asynchronously, like signal
handlers. But this is probably not an important restriction when writing a
module.
As far as raising exceptions in a module is concerned it's usually very simple. If you run into an error that you can't handle within the module just use
if ( non_recoverable_error ) THROW( EXCEPTION ); |
and fsc2
will take care of all error handling and the stop the
running experiment (or the test run). That's all you need to know about
exceptions for nearly all cases that have to be dealt within modules.
• More on programming with exceptions | ||
• Problems with exceptions |
14.4.5.1 More on programming with exceptions
Of course, when an exception is 'thrown', there must be a place where it
gets 'caught', otherwise the exception will simply kill the program. You
don't have to care about catching exceptions, fsc2
will do this
for you. But in some situations you may prefer to do it yourself. So
lets assume that you have a function foo()
that might run into a
non-recoverable error that can't be handled within the module itself.
However you may still need to do some cleaning-up before you pass it up to
fsc2
. The following example demonstrates how to catch an exception
in the calling function:
TRY { foo( ); TRY_SUCCESS; /* never forget this ! */ } CATCH( EXCEPTION ) { ... /* the error handling code goes here */ } OTHERWISE RETHROW; |
With TRY
the program is told that the following code may throw
an exception. If everything works out well and no exception is thrown
the CATCH()
and OTHERWISE
blocks are never executed and
in this case TRY_SUCCESS
must be called to do some cleaning up
(this is different from e.g. C++
where the programmer does
not have to care about this since it's done automatically). But if an
error happens and an exception (of whatever type - there are more than
the simply named EXCEPTION
exception) gets thrown the flow of
control is changed immediately from the function the exception is
thrown in to the CATCH()
line.
The CATCH
section can be used to catch a specific exception and
you can have several CATCH
blocks, one for wach different type
of exceptions you want to treat in a specific way. This should always
followed by a OTHERWISE
section that deals with exceptions of
types not having been dealt with already (unless you're completely
sure you've already handled every possible type). Of course, it's
perfectly ok to just have a OTHERWISE
block if you don't want
to handle different types of exceptions differently. Within the
OTHERWISE
block you can just rethrow the exception by calling
RETHROW
if you're not interested in it or are unable to handle
it yourself .
In cases where you aren't interested in a special type of exception but
want to catch every exception, e.g. to just do some cleaning up before
bailing out to pass the problem on to some higher level routines, you
can use just an OTHERWISE
block without a CATCH()
. Here's
another example:
TRY { do_something_error_prone(); TRY_SUCCESS; } OTHERWISE { do_local_cleanup(); /* e.g. deallocate memory */ RETHROW; } |
There are three types of exceptions that may be relevant when writing a module:
EXCEPTION OUT_OF_MEMORY_EXCEPTION USER_BREAK_EXCEPTION |
EXCEPTION
is a kind of catch-all exceptions not covered by the
other two types. OUT_OF_MEMORY_EXCEPTION
gets only thrown by
fsc2
s special functions for memory allocation (see next section),
so don't throw it yourself without a very good reason. A
USER_BREAK_EXCEPTION
can be thrown from within a module when the
module is doing something rather time consuming (e.g. waiting for a
device to become ready or doing some calibration) and the user has
pressed the STOP
button. In many cases it's probably simpler not
to throw the USER_BREAK_EXCEPTION
directly but use the function
void stop_on_user_request( void ); |
It will detect if the user has pressed the STOP
button and, if
she did, will throw an USER_BREAK_EXCEPTION
all by itself. This
works from all parts of the module except when running the
end_of_exp_hook
and exit_hook
functions because these
need to run without the user intervening. Thus you must make sure
that these clean-up functions don't call other functions that may rely
on user intervention.
But if you don't want to use stop_on_user_request()
but need to
do some cleanup within the module when the STOP
button has been
pressed you can check if the button has been pressed by calling the
function
void check_user_request( void ); |
It will return true
if the STOP
button has been pressed.
You then should do your cleanup and immediately afterwards throw a
USER_BREAK_EXCEPTION
yourself.
Here's some code taken from the module for a digitizer. It waits
indefinitely in a loop for the digitizer to become finished with a
measurement. To allow the user to get out of this loop (when, for
example, he realizes that he forgot to connect the trigger input to the
digitizer, so the function never returns) stop_on_user_request()
is called each time the loop is repeated. When the user presses the
STOP
button the function will break out of the loop by throwing
an USER_BREAK_EXCEPTION
.
while ( 1 ) /* loop forever */ { stop_on_user_request( ); fsc2_sleep( 100000, UNSET ); /* give the device a bit of time */ length = 40; if ( gpib_write( tds754a.device, "BUSY?\n", 6 ) == FAILURE || gpib_read( tds754a.device, reply, &length ) == FAILURE ) THROW( EXCEPTION ); if ( length > 0 && reply[ 0 ] == '1' ) /* break when digitizer is ready */ break; } |
14.4.5.2 Problems with exceptions
There is a catch when using exceptions. The exception mechanism is
using the standard C functions setjmp()
and longjmp()
to
realize TRY
and CATCH
. These functions have one problem:
when an exception is thrown the data stored in CPU registers are not
necessarily saved. But an optimizing compiler usually stores values of
often used variables in CPU registers, i.e. the value of a variable
in memory is not necessarily identical to its 'real' value (or
variables might even not exist in memory, they may have gotten
optimized out.). When the program now reaches the CATCH()
part
the values of these variables can be completely bogus and, if you
would try to use their values, hard to find errors may result.
Fortunately, when the compiler gets invoked with its warning level set
to a suitable level it will recognize such potential problems and emit
a warning message like the following (this example is taken from
gcc
):
module.c:123: warning: variable `i' might be clobbered by `longjmp' or `vfork' |
You might get this warning for code like this:
long ** foo( size_t count, size_t len ) { long ** buffer; size_t i; TRY { for ( i = 0; i < count; i++ ) buffer[ i ] = T_malloc( len * sizeof ** buffer ); TRY_SUCCESS; } CATCH( OUT_OF_MEMORY_EXECPTION ) { for ( --i; i >= 0; i-- ) T_free( buffer[ i ] ) RETHROW; } return buffer; } |
Chances are high that the compiler will use a register for the
variable i
and buffer
to speed up execution. But when an
exception is thrown the values i
and buffer
had in the
loop of the TRY
block may get lost in the process, even though
they're still needed.
Fortunatley, there's a simple way to take care of this problem: all
you need to do is qualify the variables as volatile
. If it's
a normal variable use e.g.
int volatile i = 0; |
and if it's a pointer use
long ** volatile buffer; |
14.4.6 Functions for memory allocation
There are special function for fsc2
for allocating memory.
These functions not only allocate memory but also check that the
allocation really returned as much memory as you asked for (on failure
the program is stopped and an appropriate error message is
printed). That means that you don't have to care for error handling -
if these memory allocation functions return everything is ok, otherwise
they won't return at all.
The first of these functions called T_malloc()
(think about it
as tested malloc). And, of course, there is also a replacement for
realloc()
and calloc()
, called T_realloc()
and
T_calloc()
. There's an also additional function for reallocation,
T_realloc_or_free()
which does the same as T_realloc()
but
additionally frees already allocated memory, so you don't have to catch
failures of allocation and deallocate the original memory it was called
with yourself.
For the duplication of strings you should use T_strdup()
instead
f the normal strdup()
. And, to make things complete, the replacement
for free()
is called T_free()
.
All functions accept the same input and return values as their normal counterparts, i.e. they have the prototypes:
void * T_malloc( size_t size ); void * T_calloc( size_t nmemb, size_t size ); void * T_realloc_or_free( void * ptr, size_t size ); void * T_realloc( void * ptr, size_t size ); char * T_strdup( const char * string ); void * T_free( void * ptr ); |
For T_free()
there's is small deviation from the behavior of
the normal free()
function. T_free()
returns a void
pointer, which is always NULL
.
There might be cases where you need a call of one of the functions for
allocation of memory to return even if it fails. In this case you have
to call the function from within a TRY
block and be prepared to
catch the OUT_OF_MEMORY_EXCEPTION
exception that gets thrown when
the memory allocation fails. Here's some example code:
TRY { array = T_malloc( length ); TRY_SUCCESS; } CATCH( OUT_OF_MEMORY_EXCEPTION ) { ... /* your error handling code goes here */ } |
14.4.7 The bool type
fsc2
already has a typedef
for the bool
type,
i.e. for variables that can have only two values, either 1
or
0
. It is declared as
typedef enum { false = 0, true = 1 } bool; |
You can use either the macros SET
, OK
or TRUE
instead of 1
and UNSET
, FAIL
or FALSE
instead of 1
. Use this type to do things like
bool is_flag; flag = SET; ... if ( ! flag ) { do_something( ); flag = UNSET; } ... if ( flag == SET ) do_something_else( ); |
14.4.8 Numerical conversions and comparisons
It's a rather common task to round floating point numbers to some type of integer (signed or unsigned). Therefore, a set of utility functions exists for the more common of these tasks. They are:
short int srnd( double a ); unsigned short usrnd( double a ); int irnd( double x ); unsigned int uirnd( double x ); long lrnd( double x ); unsigned long ulrnd( double x ); |
srnd()
, usrnd()
, irnd()
, uirnd()
, lrnd()
and ulrnd()
round a double value to the next short int
,
unsigned short int
, int
, unsigned int
, long int
or unsigned long int
value, respectively. If the double value is
smaller than the smallest value of the target range (0 for conversion to an
unsigned integer type) the turn value is the smallest value in the target
range and errno
is set to ERANGE
. If the value is too large
to fit into the traget range the return value is the largest possible value
in the target range and errno
is also set to ERANGE
(if the
conversion does not result in an out-of-range value errno
is not
modified). If you plan to check for an out-of-range error you thus have to
zero-out errno before calling the functions.
Footnotes
(26)
The basic ideas for the exceptions code came from an article by Peter Simons in the iX magazine (http://www.heise.de/ix/), No. 5, 1998, pp. 160-162, but also several other people implemented similar solutions. My version has benefited a lot from the very constructive criticism by Chris Torek (nospam@elf.eng.bsdi.com) on news:comp.lang.c (which isn't meant to say that he would be responsible for its shortcomings in any way).
This document was generated by Jens Thoms Toerring on September 6, 2017 using texi2html 1.82.