Jump to content

Programming Reference:Error Handling: Difference between revisions

From BCI2000 Wiki
Atennissen (talk | contribs)
No edit summary
Mellinger (talk | contribs)
 
(14 intermediate revisions by 3 users not shown)
Line 1: Line 1:
=='''Handling Errors'''==
==Handling Errors==


==='''Types of Errors'''===
===Types of Errors===


We assume that all errors we need to consider fall into one of the following categories, each of which implies a different type of approach to error avoidance/error handling:
We assume that all errors we need to consider fall into one of the following categories, each of which implies a different type of approach to error avoidance/error handling:
*Parameter Setup Errors
*Parameter Setup Errors
*Runtime Errors
*Runtime Errors
*Logic (Programming) Errors  
*Logic (Programming) Errors  




==='''Parameter Setup Errors'''===
===Parameter Setup Errors===


===='''Definition of the Term'''====
====Definition of the Term====
This category covers anything a user can do wrong by using a program with parameters that are out of range, inconsistent, or otherwise erroneous (e.g., by specifying an output file at a location where the user has no write permission).
This category covers anything a user can do wrong by using a program with parameters that are out of range, inconsistent, or otherwise erroneous (e.g., by specifying an output file at a location where the user has no write permission).


Parameter setup errors, when unhandled, become runtime errors.
Parameter setup errors, when unhandled, become runtime errors.


===='''Strategies'''====
====Strategies====


As a guideline for approaching Parameter Setup Errors we adopt the following principle: "Whatever a
As a guideline for approaching Parameter Setup Errors we adopt the following principle: "Whatever a
user does from within an application program should never make that application crash."
user does from within an application program should never make that application crash."


In BCI 2000, this translates into a thorough parameter check done by each module before any parameter settings are actually applied to the system.
In BCI2000, this translates into a thorough parameter check done by each module before any parameter settings are actually applied to the system.


Parameter checking should comprise
Parameter checking should comprise
 
*Range and Consistency checks, whereby ranges generally depend on the values of other parameters;  
*Range and Consistency checks, whereby ranges generally depend on other parameters' values;  
 
*Signal property checks: Does the output signal of one filter meet the next filter's requirements for its input signal?
*Signal property checks: Does the output signal of one filter meet the next filter's requirements for its input signal?
*Resource availability checks:
*Resource availability checks:
 
**Are needed system resources available? (E.g., is it possible to open a required sound output device?)
-Are needed system resources available? (E.g., is it possible to open a required sound output device?)
**Are auxiliary files (e.g., media files) available and readable?
 
**Do output files have legal file names? Are output files writeable? (We could even check whether there is enough space left to write the EEG file, but this is not practical because a concurrent process might use up the space while our system runs.)  
-Are auxiliary files (e.g., media files) available and readable?)
 
-Do output files have legal file names? Are output files writeable? (We could even check whether
there is enough space left to write the EEG file, but this is not practical because a  
concurrent process might use up the space while our system runs.)  




Line 48: Line 37:


*it presents the user with a warning that tells her what it did and why it did so, and if  
*it presents the user with a warning that tells her what it did and why it did so, and if  
*the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check performed on them.  
*the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check performed on them.  


Line 54: Line 42:
a system that creates new parameter inconsistencies when trying to fix others.
a system that creates new parameter inconsistencies when trying to fix others.


===='''User Interface Details'''====
====User Interface Details====


The user interface for Parameter Setup Error handling is, along with the parameter setup dialog, part of the operator module. A first implementation of a GUI based user interface consists in a floating, non-modal error window popping up from the operator module that presents a list of error related textual messages to the user, allowing for browsing error messages
The user interface for Parameter Setup Error handling is, along with the parameter setup dialog, part of the operator module. A first implementation of a GUI based user interface consists in a floating, non-modal error window popping up from the operator module that presents a list of error related textual messages to the user, allowing for browsing error messages
Line 61: Line 49:




==='''Runtime Errors'''===
===Runtime Errors===


===='''Definition of the Term'''====
====Definition of the Term====


This category covers everything that can go wrong in the course of running an application program,  
This category covers everything that can go wrong in the course of running an application program,  
Line 69: Line 57:


Typical reasons for this kind of error are
Typical reasons for this kind of error are
 
*the system runs out of disk space while recording data,  
*the system runs out of disk space while recording data,  
*files being moved, trashed, or locked by a concurrent process,  
*files being moved, trashed, or locked by a concurrent process,  
*a network connection becomes unavailable.  
*a network connection becomes unavailable.  


Runtime errors, when unhandled, become logic errors because the code implies assumptions that no longer hold once a runtime error has occurred.
Runtime errors, when unhandled, become logic errors because the code implies assumptions that no longer hold once a runtime error has occurred.


===='''Strategies'''====
====Strategies====


In a properly designed and implemented system, runtime errors, in the restricted sense described above, will not occur frequently. However, as they are caused by undesired circumstances outside the scope of the application program itself, it seems important to provide information to the user that is as detailed as possible in order to enable her to prevent this type of situation in the future, and to make her aware of the fact that the application program depends on her willingness to provide a smooth operating environment. That being said, it seems appropriate to simply abort execution altogether, while trying to avoid a loss of the data acquired up to that time.
In a properly designed and implemented system, runtime errors, in the restricted sense described above, will not occur frequently. However, as they are caused by undesired circumstances outside the scope of the application program itself, it seems important to provide information to the user that is as detailed as possible in order to enable her to prevent this type of situation in the future, and to make her aware of the fact that the application program depends on her willingness to provide a smooth operating environment. After displaying a message of this kind, it seems appropriate to simply abort execution altogether, while trying to avoid a loss of the data acquired up to that time.


===='''User Interface Details'''====
====User Interface Details====


In general, it is desirable to have runtime errors displayed along with the operator module's user interface. However, as this requires a working connection between the module where the error occurs, and the operator module, this may not always be possible. Therefore, in addition to an perator-based error reporting interface, each module should have a less demanding mechanism to provide error information to the user, e.g., a local log file.
In general, it is desirable to have runtime errors displayed along with the user interface of the operator module. However, as this requires a working connection between the module where the error occurs, and the operator module, this may not always be possible. Therefore, in addition to an operator-based error reporting interface, each module should have a less demanding mechanism to provide error information to the user, e.g., a local log file.




==='''Logic Errors'''===
===Logic Errors===


===='''Definition of the Term'''====
====Definition of the Term====


Logic, or programming, errors in general can be due to a programmer who, in his or her code, implicitly or explicitly makes assumptions that do not always hold.
Logic, or programming, errors in general can be due to a programmer who, in his or her code, implicitly or explicitly makes assumptions that do not always hold.


===='''Strategies'''====
====Strategies====


Programming errors are not supposed to occur at all in a tested version of an application. Therefore, instead of trying to 'handle' them, it is important to make them show up as close to their point of origin in the code as possible, by frequently and explicitly checking whether
Programming errors are not supposed to occur at all in a tested version of an application. Therefore, instead of trying to 'handle' them, it is important to make them show up as close to their point of origin in the code as possible, by frequently and explicitly checking whether
implicit assumptions actually hold, and aborting execution with an error message if this is not the case.  
implicit assumptions actually hold, and aborting execution with an error message if this is not the case.
 
Typically, such checks use an "assertion" facility provided by the programming language. BCI2000 provides its own <tt>bciassert</tt> macro in <tt>BCIAssert.h</tt> to make sure that failed assertions result in messages that are displayed by the BCI2000 operator module. Unlike the standard assert macro, BCI2000 assertions do not evaluate to empty in release builds.


Aside from that, writing code as explicit, general, and simple as possible greatly reduces the possibility of making logic errors in the first place.
Aside from that, writing code as explicit, general, and simple as possible greatly reduces the possibility of making logic errors in the first place.


===='''User Interface Details'''====
====User Interface Details====


As programming errors are errors about which a user can do nothing, simply aborting the program or module with an error message seems appropriate.
As programming errors are errors about which a user can do nothing, simply aborting the program or module with an error message seems appropriate.




=='''Implementation Details'''==
==Implementation Details==


==='''Interface to the Programmer'''===
===Interface to the Programmer===


===='''Reporting Errors'''====
====Reporting Errors====


For a simple and general way to provide user communication and a means of error reporting to a module's programmer, there exist two global objects derived from <tt>std::ostream</tt>, named
For a simple and general way to provide user communication and a means of error reporting to a module's programmer, there exist two global objects derived from <tt>std::ostream</tt>, one named
<tt>bciout</tt> and <tt>bcierr</tt>, in analogy to <tt>std::cout</tt> and <tt>std::cerr</tt>, where <tt>bciout</tt> is used to transfer general messages and warnings while <tt>bcierr</tt> takes actual errors.
<tt>bciout</tt> and the other named <tt>bcierr</tt>, analogous to <tt>std::cout</tt> and <tt>std::cerr</tt>, where <tt>bciout</tt> is used to transfer general messages and warnings while <tt>bcierr</tt> takes actual errors.
A code example then looks like this:
A code example then looks like this:


  #include "BCIError.h"
  ...
   using namespace std;
   using namespace std;
   ...
   ...
Line 120: Line 109:
   if( !outputStream.is_open() )
   if( !outputStream.is_open() )
   {
   {
  bcierr << "Cannot open the file \""
    bcierr << "Cannot open the file \""
        << fileName
          << fileName
        << "\" for output"
          << "\" for output"
        << endl;
          << endl;
   }
   }


Furthermore, for handling runtime errors from which it is difficult to recover, a programmer may program an exception that will abort execution and eventually lead to an error message being sent to the operator module (for framework related details see section 2.2):
Furthermore, for handling runtime errors from which it is difficult to recover, code may throw an exception that will abort execution and eventually lead to an error message being sent to the operator module (for framework related details see the section entitled "Implementation on the Framework Side"):


#include "BCIException.h"
  ...
  ...
  if( ernie.find( bert ) != ernie.end() )
  if( ernie.find( bert ) != ernie.end() )
     throw "Ernie just ate Bert. "
     throw bciexception( "Ernie just ate Bert. I don't know how to tell the story." );
              "I don't know how to tell the story.";
  tellMyStory( ernie, bert );
  tellMyStory( bert.begin(), ernie.end() );
  ...
  ...


===='''Checking Parameters'''====
====Checking Parameters====


Checking parameters is done in a separate member function of the filter base class which, similar to the member function that does the actual processing, takes input and output signal representatives as parameters, thus allowing for signal property checking.
Checking parameters is done in a separate member function of the filter base class which, similar to the member function that does the actual processing, takes input and output signal representatives as parameters, thus allowing for signal property checking.
Line 141: Line 130:
For the actual implementation, its declaration is as follows:
For the actual implementation, its declaration is as follows:


<tt>void GenericFilter::Preflight ( const SignalProperties& inSignalProperties, SignalProperties& outSignalProperties ) const;</tt>
void GenericFilter::Preflight ( const SignalProperties& Input,  
                                      SignalProperties& Output ) const;


For a filter class derived from <tt>GenericFilter</tt>, this function is supposed to perform
For a filter class derived from <tt>GenericFilter</tt>, this function is supposed to perform
parameter checking as described in section 1.2.2. Instead of returning an error value, it writes possible error messages into <tt>bcierr</tt>. Furthermore, it communicates dimensions of its output signal which it guarantees not to exceed, and it does so by adjusting the properties of the second SignalProperties object in its argument list, e.g.
parameter checking as described in the "Strategies" section. Instead of returning an error value, it writes possible error messages into <tt>bcierr</tt>. Furthermore, it communicates dimensions of its output signal which it guarantees not to exceed, and it does so by adjusting the properties of the second SignalProperties object in its argument list, e.g.


<tt>outSignalProperties
Output = SignalProperties( Input.Channels(), 1 );
= SignalProperties( inSignalProperties.Channels(), 1 );</tt>


or
or


<tt>outSignalProperties = SignalProperties( 0, 0 );</tt>
Output = SignalProperties( 0, 0 );


if it declares not to use its output signal.
if it declares not to use its output signal.


The <tt>const</tt> declaration for its <tt>this</tt> pointer prohibits initialization functionality from <tt>GenericFilter::Initialize()</tt> entering into <tt>Preflight()</tt>; this is unwanted because it would corrupt the idea of performing a ''complete''  parameter check before actually ''altering'' the state of a filter object.
The <tt>const</tt> declaration for its <tt>this</tt> pointer prohibits initialization functionality from <tt>GenericFilter::Initialize()</tt> entering into <tt>Preflight()</tt>. Such behavior is unwanted because it would corrupt the idea of performing a ''complete''  parameter check before actually ''altering'' the state of a filter object.


A necessary condition for a correct implementation of the <tt>Preflight()</tt> function is that any parameter, as well as any state that will be accessed during the processing phase, be accessed
A necessary condition for a correct implementation of the <tt>Preflight()</tt> function is that any parameter, as well as any state that will be accessed during the processing phase, be accessed
from <tt>Preflight()</tt> at least once. For parameters and states defined by the filter itself (i.e. inside its constructor), range and accessibility checks are automatically performed by the framework; parameters and states defined by other filters must be explicitly accessed from <tt>Preflight()</tt>. If a <tt>GenericFilter</tt> descendant fails to access an externally defined parameter or state during <tt>Preflight()</tt>, the first access during the processing phase will result in a runtime error.
from <tt>Preflight()</tt> at least once. For parameters and states defined by the filter itself (i.e. inside its constructor), range and accessibility checks are automatically performed by the framework; parameters and states defined by other filters must be explicitly accessed from <tt>Preflight()</tt>. If a <tt>GenericFilter</tt> descendant fails to access an externally defined parameter or state during <tt>Preflight()</tt>, the first access during the processing phase will result in a runtime error.


===='''Accessing Environment Objects'''====
====Accessing Environment Objects====


We consider parameters and states part of the bci2000 "environment". <tt>GenericFilter</tt> descendants have access to that environment, analgous to the concept of environment variables found in some operating systems. Internally, access to the environment is mediated through a mix-in-class named <tt>Environment</tt> that provides accessor symbols to a filter programmer.
We consider parameters and states part of the BCI2000 "environment". <tt>GenericFilter</tt> descendants have access to that environment, analogous to the concept of environment variables found in some operating systems. Internally, access to the environment is mediated through a mix-in-class named <tt>Environment</tt> that provides accessor symbols to a filter programmer.


'''Low Level Access to Environment Objects''' is provided by the following symbols:
'''Low Level Access to Environment Objects''' is provided by the following symbols:
 
*<tt>Parameters</tt> syntactically behaves like a <tt>ParamList*</tt>,  
*<tt>Parameters</tt> syntactically behaves like a <tt>PARAMLIST*</tt>,  
*<tt>States</tt> behaves like a <tt>StateList*</tt>,  
 
*and <tt>Statevector</tt> behaves like a <tt>StateVector*</tt>.  
*<tt>States</tt> behaves like a <tt>STATELIST*</tt>,  
 
*and <tt>Statevector</tt> behaves like a <tt>STATEVECTOR*</tt>.  


As an example,  
As an example,  


  float  myParameterValue = 0.0;
  float  myParameterValue = 0.0;
  PARAM* param = Parameters->GetParamPtr( "MyParameter" );
  Param* param = Parameters->GetParamPtr( "MyParameter" );
  if( param )
  if( param )
myParameterValue = atof( param->GetValue() );
  myParameterValue = atof( param->GetValue() );
  else
  else
bcierr << "Could not access \"MyParameter\"" << endl;
  bcierr << "Could not access \"MyParameter\"" << endl;


Unlike true pointers, these symbols cannot be assigned any values, cannot be assigned to variables, or have other manipulating operators applied, e.g., the lines
Unlike true pointers, these symbols cannot be assigned any values, cannot be assigned to variables, or have other manipulating operators applied. For example, the lines


  <tt>delete Parameters;
  delete Parameters;
  Parameters = new PARAMLIST;
  Parameters = new ParamList;
PARAMLIST* someParamlistPointer = Parameters;</tt>
 
will all result in compiler errors.
will all result in compiler errors. (In the current (preliminary) implementation, assignments from these symbols, as in the last example, are allowed to ease the transition process.)


'''Convenient Access to Environment Objects''' is possible through a number of symbols which offer built-in checking and error reporting:
'''Convenient Access to Environment Objects''' is possible through a number of symbols which offer built-in checking and error reporting:
    
    
*{<tt>Parameter(Name[, Index 1[, Index 2]])</tt>}
*<tt>Parameter(Name)[(Index 1[, Index 2])</tt>  


This symbol stands for the value of the named parameter.  Indices may be given in numerical or textual form; if omitted, they default to 0. The type of the symbol <tt>Parameter()</tt> may be numerical or a string type, depending on its use.\footnote{If the compiler complains about ambiguities, use explicit typecasts as in the second example.} If a parameter with the given name does not exist, an error message is written into <tt>bcierr</tt>. If the specified indices do not exist, no error is reported. In both cases, on read access, the string constant <tt>"0"</tt> resp. the number 0 is returned.
This symbol stands for the value of the named parameter.  Indices may be given in numerical or textual form; if omitted, they default to 0. The type of the symbol <tt>Parameter()</tt> may be numerical or a string type, depending on its use. (If the compiler complains about ambiguities, use explicit typecasts.) If a parameter with the given name does not exist, an error message is written into <tt>bcierr</tt>. If the specified indices do not exist, no error is reported. In both cases, on read access, the string constant <tt>"0"</tt> resp. the number 0 is returned.
(If the compiler complains about ambiguities, use explicit typecasts as in the second example.)


Examples:  
Examples:  


  <tt>int myValue = Parameter( "MyParam" );  
  int myValue = Parameter( "MyParam" );  
  string myOtherValue = ( const char* )Parameter( "MyOtherParam" );  
  string myOtherValue = Parameter( "MyOtherParam" );  
  Parameter( "My3rdParam", 2, 3 ) = my3rdValue;</tt> 
  Parameter( "My3rdParam" )( 2, 3 ) = my3rdValue;  


*<tt>OptionalParameter(Default Value, Name[, Index 1[, Index 2]])</tt>  
*<tt>OptionalParameter(Name[, Default Value])(Index 1[, Index 2])</tt>  


This symbol behaves like the symbol <tt>Parameter()</tt> but will not report an error if the parameter does not exist. Instead, it will return the default value given in its first argument. Assignments to this symbol are not possible.  
This symbol behaves like the symbol <tt>Parameter()</tt> but will not report an error if the parameter does not exist. Instead, it will return the default value given in its first argument. Assignments to this symbol are not possible.  


*<tt>State(Name)
*<tt>State(Name)</tt>


</tt> This symbol allows for reading a state's value from the state vector and setting a state's value in the state vector. Trying to access a state that is not accessible will result in an error reported via <tt>bcierr</tt>.  
This symbol allows for reading a state's value from the state vector and setting a state's value in the state vector. Trying to access a state that is not accessible will result in an error reported via <tt>bcierr</tt>.  


Examples:
Examples:


  <tt>short currentStateOfAffairs = State( "OfAffairs" );  
  short currentStateOfAffairs = State( "OfAffairs" );  
  State( "OfAffairs" ) = nextStateOfAffairs;</tt>
  State( "OfAffairs" ) = nextStateOfAffairs;


*<tt>OptionalState(Default Value, Name)
*<tt>OptionalState(Name[, Default Value])</tt>


</tt> Analagous to <tt>OptionalParameter()</tt>, this symbol does not report an error if the specified state does not exist but returns the given default value. Assignments to this symbol are not possible.  
Analagous to <tt>OptionalParameter()</tt>, this symbol does not report an error if the specified state does not exist but returns the given default value. Assignments to this symbol are not possible.  


*<tt>PreflightCondition(Condition)</tt>
*<tt>PreflightCondition(Condition)</tt>
Line 229: Line 213:
If TransmitCh is greater than SourceCh, a message will be sent to <tt>bcierr</tt> and  displayed to the user, stating:  
If TransmitCh is greater than SourceCh, a message will be sent to <tt>bcierr</tt> and  displayed to the user, stating:  


<tt>Condition not fulfilled:   
<tt>A necessary condition is violated. Please make sure that the following is true:   
Parameter( "TransmitCh" ) <= Parameter( "SourceCh" )</tt>  
Parameter( "TransmitCh" ) <= Parameter( "SourceCh" )</tt>


(In future versions, the error may be reported in natural language form generated from the boolean expression.)
===Implementation on the Framework Side===
==='''Implementation on the Framework Side'''===


The operator module's behaviour in response to an error message that arrives from one of the modules depends on its context, i.e. on the execution phase the system is in. That way, no additional programming interface elements visible to a filter/module programmer are needed to implement an error handling scheme as described in section 1.  
The behaviour of the operator module in response to an error message that arrives from one of the modules depends on its context, i.e., on the execution phase the system is in. That way, no additional programming interface elements visible to a filter/module programmer are needed to implement an error handling scheme as described in the "Handling Errors"section.  


During the '''preflight phase,'''  errors are {Parameter Setup Errors.} A module's framework code behind <tt>bcierr</tt> just collects error messages; on return from the preflight function, it
During the '''preflight phase,'''  errors are ''Configuration Errors''. A module's framework code behind <tt>bcierr</tt> just collects error messages; on return from the preflight function, it
sends those messages to the operator module which then, from the contents of the message (i.e. whether it was empty or not), determines whether the preflight was successful; on not receiving any message after some timeout it assumes a broken connection or a crashed module.
sends those messages to the operator module which then, from the contents of the message (i.e., whether it was empty or not), determines whether the preflight was successful; on not receiving any message after some timeout it assumes a broken connection or a crashed module.


(For now, a simple timeout scheme with a fixed timeout interval of 5s seems appropriate. In the future, one might consider a module requesting additional timeout periods if it expects lengthy calculations.)
(For now, a simple timeout scheme with a fixed timeout interval of 5s seems appropriate. In the future, one might consider a module requesting additional timeout periods if it expects lengthy calculations.)


During all '''other phases,'''  the code behind <tt>bcierr</tt> immediately (i.e., on flushing the <tt>std::ostream</tt>) sends its message buffer to a log file as well as to the operator module, indicating a {Runtime Error} to the operator module which will, in turn, halt the system,shut down the other modules, and display the message to the user.
During all '''other phases,'''  the code behind <tt>bcierr</tt> immediately (i.e., on flushing the <tt>std::ostream</tt>) sends its message buffer to a log file as well as to the operator module, indicating a ''Runtime Error'' to the operator module which will, in turn, halt the system,shut down the other modules, and display the message to the user.


In addition, the top level exception handling code of each module contains similar functionality,
In addition, the top level exception handling code of each module contains similar functionality,
sending an exception's associated description string into a log file and to the operator module, if possible, then quitting the module in which the exception occurred. This not only ensures
sending an exception's associated description string into a log file and to the operator module, if possible, then quitting the module in which the exception occurred. This not only ensures
a proper general handling of exceptions within the framework but also allows a programmer to handle {Runtime Errors} by raising her own exceptions, eliminating the need to take care of the error
a proper general handling of exceptions within the framework but also allows a programmer to handle ''Runtime Errors'' by raising her own exceptions, eliminating the need to take care of the error
condition in the code following the detection of an error.
condition in the code following the detection of an error.
----
==See also==
[[Programming Reference:Errors and Warnings]], [[Programming Reference:Environment Class]], [[Programming Reference:Debug Output]]
[[Category:Framework API]][[Category:Development]]

Latest revision as of 14:19, 12 August 2011

Handling Errors

Types of Errors

We assume that all errors we need to consider fall into one of the following categories, each of which implies a different type of approach to error avoidance/error handling:

  • Parameter Setup Errors
  • Runtime Errors
  • Logic (Programming) Errors


Parameter Setup Errors

Definition of the Term

This category covers anything a user can do wrong by using a program with parameters that are out of range, inconsistent, or otherwise erroneous (e.g., by specifying an output file at a location where the user has no write permission).

Parameter setup errors, when unhandled, become runtime errors.

Strategies

As a guideline for approaching Parameter Setup Errors we adopt the following principle: "Whatever a user does from within an application program should never make that application crash."

In BCI2000, this translates into a thorough parameter check done by each module before any parameter settings are actually applied to the system.

Parameter checking should comprise

  • Range and Consistency checks, whereby ranges generally depend on the values of other parameters;
  • Signal property checks: Does the output signal of one filter meet the next filter's requirements for its input signal?
  • Resource availability checks:
    • Are needed system resources available? (E.g., is it possible to open a required sound output device?)
    • Are auxiliary files (e.g., media files) available and readable?
    • Do output files have legal file names? Are output files writeable? (We could even check whether there is enough space left to write the EEG file, but this is not practical because a concurrent process might use up the space while our system runs.)


In each of these cases, the user should get appropriate feedback guiding her towards fixing the problem.

Whenever the system tries to fix a parameter setup error by using some default set of parameters, it should do so only if

  • it presents the user with a warning that tells her what it did and why it did so, and if
  • the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check performed on them.

Otherwise, people may unknowingly using a system that doesn't do what they want it to, or have a system that creates new parameter inconsistencies when trying to fix others.

User Interface Details

The user interface for Parameter Setup Error handling is, along with the parameter setup dialog, part of the operator module. A first implementation of a GUI based user interface consists in a floating, non-modal error window popping up from the operator module that presents a list of error related textual messages to the user, allowing for browsing error messages while changing respective parameters via the parameter setup dialog. After the next parameter check, the operator module will close that window or replace its contents based on the result of the check. Parameter checking occurs when the user clicks the "SetConfig" button in the operator main window, followed by actually applying parameters if the check was successful.


Runtime Errors

Definition of the Term

This category covers everything that can go wrong in the course of running an application program, insofar as that malfunction is due to a lack of resources in the underlying system required for proper operation (i.e., not due to a programming error). Assuming that parameter checking has been implemented properly as outlined above, we can narrow the term `Runtime Error' to cases for which the following statement holds: A runtime error occurs whenever the system runs out of resources that were still available during parameter checking.

Typical reasons for this kind of error are

  • the system runs out of disk space while recording data,
  • files being moved, trashed, or locked by a concurrent process,
  • a network connection becomes unavailable.

Runtime errors, when unhandled, become logic errors because the code implies assumptions that no longer hold once a runtime error has occurred.

Strategies

In a properly designed and implemented system, runtime errors, in the restricted sense described above, will not occur frequently. However, as they are caused by undesired circumstances outside the scope of the application program itself, it seems important to provide information to the user that is as detailed as possible in order to enable her to prevent this type of situation in the future, and to make her aware of the fact that the application program depends on her willingness to provide a smooth operating environment. After displaying a message of this kind, it seems appropriate to simply abort execution altogether, while trying to avoid a loss of the data acquired up to that time.

User Interface Details

In general, it is desirable to have runtime errors displayed along with the user interface of the operator module. However, as this requires a working connection between the module where the error occurs, and the operator module, this may not always be possible. Therefore, in addition to an operator-based error reporting interface, each module should have a less demanding mechanism to provide error information to the user, e.g., a local log file.


Logic Errors

Definition of the Term

Logic, or programming, errors in general can be due to a programmer who, in his or her code, implicitly or explicitly makes assumptions that do not always hold.

Strategies

Programming errors are not supposed to occur at all in a tested version of an application. Therefore, instead of trying to 'handle' them, it is important to make them show up as close to their point of origin in the code as possible, by frequently and explicitly checking whether implicit assumptions actually hold, and aborting execution with an error message if this is not the case.

Typically, such checks use an "assertion" facility provided by the programming language. BCI2000 provides its own bciassert macro in BCIAssert.h to make sure that failed assertions result in messages that are displayed by the BCI2000 operator module. Unlike the standard assert macro, BCI2000 assertions do not evaluate to empty in release builds.

Aside from that, writing code as explicit, general, and simple as possible greatly reduces the possibility of making logic errors in the first place.

User Interface Details

As programming errors are errors about which a user can do nothing, simply aborting the program or module with an error message seems appropriate.


Implementation Details

Interface to the Programmer

Reporting Errors

For a simple and general way to provide user communication and a means of error reporting to a module's programmer, there exist two global objects derived from std::ostream, one named bciout and the other named bcierr, analogous to std::cout and std::cerr, where bciout is used to transfer general messages and warnings while bcierr takes actual errors. A code example then looks like this:

 #include "BCIError.h"
 ...
 using namespace std;
 ...
 ofstream outputStream( fileName );
 if( !outputStream.is_open() )
 {
   bcierr << "Cannot open the file \""
          << fileName
          << "\" for output"
          << endl;
 }

Furthermore, for handling runtime errors from which it is difficult to recover, code may throw an exception that will abort execution and eventually lead to an error message being sent to the operator module (for framework related details see the section entitled "Implementation on the Framework Side"):

#include "BCIException.h"
...
if( ernie.find( bert ) != ernie.end() )
   throw bciexception( "Ernie just ate Bert. I don't know how to tell the story." );
tellMyStory( ernie, bert );
...

Checking Parameters

Checking parameters is done in a separate member function of the filter base class which, similar to the member function that does the actual processing, takes input and output signal representatives as parameters, thus allowing for signal property checking.

For the actual implementation, its declaration is as follows:

void GenericFilter::Preflight ( const SignalProperties& Input, 
                                      SignalProperties& Output ) const;

For a filter class derived from GenericFilter, this function is supposed to perform parameter checking as described in the "Strategies" section. Instead of returning an error value, it writes possible error messages into bcierr. Furthermore, it communicates dimensions of its output signal which it guarantees not to exceed, and it does so by adjusting the properties of the second SignalProperties object in its argument list, e.g.

Output = SignalProperties( Input.Channels(), 1 );

or

Output = SignalProperties( 0, 0 );

if it declares not to use its output signal.

The const declaration for its this pointer prohibits initialization functionality from GenericFilter::Initialize() entering into Preflight(). Such behavior is unwanted because it would corrupt the idea of performing a complete parameter check before actually altering the state of a filter object.

A necessary condition for a correct implementation of the Preflight() function is that any parameter, as well as any state that will be accessed during the processing phase, be accessed from Preflight() at least once. For parameters and states defined by the filter itself (i.e. inside its constructor), range and accessibility checks are automatically performed by the framework; parameters and states defined by other filters must be explicitly accessed from Preflight(). If a GenericFilter descendant fails to access an externally defined parameter or state during Preflight(), the first access during the processing phase will result in a runtime error.

Accessing Environment Objects

We consider parameters and states part of the BCI2000 "environment". GenericFilter descendants have access to that environment, analogous to the concept of environment variables found in some operating systems. Internally, access to the environment is mediated through a mix-in-class named Environment that provides accessor symbols to a filter programmer.

Low Level Access to Environment Objects is provided by the following symbols:

  • Parameters syntactically behaves like a ParamList*,
  • States behaves like a StateList*,
  • and Statevector behaves like a StateVector*.

As an example,

float  myParameterValue = 0.0;
Param* param = Parameters->GetParamPtr( "MyParameter" );
if( param )
  myParameterValue = atof( param->GetValue() );
else
  bcierr << "Could not access \"MyParameter\"" << endl;

Unlike true pointers, these symbols cannot be assigned any values, cannot be assigned to variables, or have other manipulating operators applied. For example, the lines

delete Parameters;
Parameters = new ParamList;

will all result in compiler errors.

Convenient Access to Environment Objects is possible through a number of symbols which offer built-in checking and error reporting:

  • Parameter(Name)[(Index 1[, Index 2])

This symbol stands for the value of the named parameter. Indices may be given in numerical or textual form; if omitted, they default to 0. The type of the symbol Parameter() may be numerical or a string type, depending on its use. (If the compiler complains about ambiguities, use explicit typecasts.) If a parameter with the given name does not exist, an error message is written into bcierr. If the specified indices do not exist, no error is reported. In both cases, on read access, the string constant "0" resp. the number 0 is returned.

Examples:

int myValue = Parameter( "MyParam" ); 
string myOtherValue = Parameter( "MyOtherParam" ); 
Parameter( "My3rdParam" )( 2, 3 ) = my3rdValue; 
  • OptionalParameter(Name[, Default Value])(Index 1[, Index 2])

This symbol behaves like the symbol Parameter() but will not report an error if the parameter does not exist. Instead, it will return the default value given in its first argument. Assignments to this symbol are not possible.

  • State(Name)

This symbol allows for reading a state's value from the state vector and setting a state's value in the state vector. Trying to access a state that is not accessible will result in an error reported via bcierr.

Examples:

short currentStateOfAffairs = State( "OfAffairs" ); 
State( "OfAffairs" ) = nextStateOfAffairs;
  • OptionalState(Name[, Default Value])

Analagous to OptionalParameter(), this symbol does not report an error if the specified state does not exist but returns the given default value. Assignments to this symbol are not possible.

  • PreflightCondition(Condition)

This symbol is meant to be used inside implementations of GenericFilter::Preflight(). If the boolean condition given as its argument is false, it will output an error message into bcierr containing the condition given in its argument.

Example:

PreflightCondition( Parameter( "TransmitCh" ) <= Parameter( "SourceCh" ) );

If TransmitCh is greater than SourceCh, a message will be sent to bcierr and displayed to the user, stating:

A necessary condition is violated. Please make sure that the following is true: Parameter( "TransmitCh" ) <= Parameter( "SourceCh" )

Implementation on the Framework Side

The behaviour of the operator module in response to an error message that arrives from one of the modules depends on its context, i.e., on the execution phase the system is in. That way, no additional programming interface elements visible to a filter/module programmer are needed to implement an error handling scheme as described in the "Handling Errors"section.

During the preflight phase, errors are Configuration Errors. A module's framework code behind bcierr just collects error messages; on return from the preflight function, it sends those messages to the operator module which then, from the contents of the message (i.e., whether it was empty or not), determines whether the preflight was successful; on not receiving any message after some timeout it assumes a broken connection or a crashed module.

(For now, a simple timeout scheme with a fixed timeout interval of 5s seems appropriate. In the future, one might consider a module requesting additional timeout periods if it expects lengthy calculations.)

During all other phases, the code behind bcierr immediately (i.e., on flushing the std::ostream) sends its message buffer to a log file as well as to the operator module, indicating a Runtime Error to the operator module which will, in turn, halt the system,shut down the other modules, and display the message to the user.

In addition, the top level exception handling code of each module contains similar functionality, sending an exception's associated description string into a log file and to the operator module, if possible, then quitting the module in which the exception occurred. This not only ensures a proper general handling of exceptions within the framework but also allows a programmer to handle Runtime Errors by raising her own exceptions, eliminating the need to take care of the error condition in the code following the detection of an error.


See also

Programming Reference:Errors and Warnings, Programming Reference:Environment Class, Programming Reference:Debug Output