5.5. Tracing, Exception Handling and Checked Object Construction/Destruction

The ML is a library of base classes that many modules and applications use to implement image processing algorithms. In such a complex system, mechanisms to catch, log and handle runtime errors and crashes as well as mechanisms to trace program execution are required. Especially for critical or potentially unsafe functionality, support for additional checks and controls must be provided. The following paragraph describes some macros that allow for the implementation of highly safe source code with crash and error logging especially for critical functionality:

Tracing Program Execution:

  1. ML_TRACE_IN("<FunctionDescription>")

    This macro should be implemented as the first line in all functions and methods that are not very time-critical. When this code is compiled in release mode, it implements functionality that pushes a reference to the string <FunctionDescription> on a stack and into a list, and when the function is finished the pushed information is popped from the stack. This push/pop/list functionality is implemented in the classes Trace and TraceBuffer in project MLUtilities. The ML error handler (see Section 5.4, “The Class ErrorOutput and Configuring Message Outputs”) can be configured to append the list of recently called functions (trace list dumps) and the current call stack (trace stack dumps) to the registered error output callbacks for additional bug analysis.

    Note that this macro only uses about 8 simple CPU instructions in release code and thus can be added to most functions without significant performance loss.

  2. ML_TRACE_IN_TIME_CRITICAL("<FunctionDescription>")

    This macro is identical with the macro ML_TRACE_IN(""), however, it is only compiled if explicitly enabled for diagnostic purposes. In normal debug or release mode, this tracing macro is not compiled. It is especially useful for tracing time-critical functionality which is assumed to operate safely in normal mode.

Handling Exceptions:

  1. ML_TRY {

    This macro opens a source code region to be checked for undesired exceptions. If such an exception occurs, the closing ML_CATCH*() macro implements crash handling and error logging with the ML error manager and memory cleanup.

  2. } ML_CATCH()

    This macro can be used to close an ML_TRY { code fragment. The macro sends a fatal error to the ML error manager with ML_PRINT_FATAL_ERROR() and continues with the execution of the memory manager which is returned by that macro. It is typically used when no resources that were opened or allocated in the enclosed code need to be cleaned up.

  3. } ML_CATCH_RETURN_NULL()

    This is another macro that can be used to close an ML_TRY { code fragment. It is identical with } ML_CATCH(), but it returns 0.

  4. ML_CATCH_BLOCK(<exception type>){ <handling code> }

    This is another macro that can be used to close an ML_TRY { code fragment and that allows for cleaning up resources opened or allocated in the enclosed code. Multiple implementations of ML_CATCH_BLOCK() can be implemented one after another to handle different types of exceptions.

    Note that ML_CATCH_BLOCK() does not post errors to the ML error manager; this must be done explicitly in the <handling code> section if necessary.

[Important]Important

The macros listed above implement exception catching and error posting only if the code is compiled in release mode.

In debug mode, the macros result in dummy code which does not perform exception handling or catching, i.e., errors and exceptions will cause normal program crashes. This strategy has been chosen to simplify debugging in debug mode, because detecting precise error positions becomes more difficult in many debugging tools when exception handling is enabled.

The following code fragments demonstrate tracing and exception handling:

Example 5.6. Example of a Typical Use of the ML_TRACE_IN() Without Exception Catching

void MyClass::testFunction1()
{
  ML_TRACE_IN("void MyClass::testFunction1()");
  <function body>
}

Example 5.7. Example of a Typical Use of the ML_TRACE_IN() with Exception Catching

void MyClass::testFunction2()
{
  ML_TRACE_IN("void MyClass::testFunction2()");
  ML_TRY
  {
     <The function body is implemented here. If an exception
      is thrown in it then ML_CATCH posts a fatal error to
      the ML error manager, and - if the error manager does
      not terminate the process - continues execution normally>
  }
  ML_CATCH;        // This catches the error, posts it and continues
                   // if the ML error manager continues execution
}

Example 5.8. Example of a Typical Use of the ML_CATCH_RETURN_NULL()

int MyClass::testFunction3()
{
  ML_TRACE_IN("int MyClass::testFunction3()");
  ML_TRY
  {
     <The function code is implemented here. If an exception
      is thrown in it then ML_CATCH_RETURN_NULL posts a fatal error to
      the ML error manager, and - if the error manager does
      not terminate the process - continues execution with a
      returning 0>

    return result; // This is the return statement in case of successful execution.
  }
  ML_CATCH_RETURN_NULL;     // This catches the error, posts it and returns
                   // 0 if the ML error manager continues execution
}

[Note]Note

The semicolons behind the ML_TRACE_IN() macros can be omitted but are useful for an automatic code indention by the development environment.

Constructing and Deleting Objects:

  1. ML_CHECK_NEW(ptr, expression)

    This implements a new of the passed expression. In release mode, it handles the exception with an ML_PRINT_FATAL_ERROR post to the ML error manager. The pointer must have been set to NULL before.

  2. ML_CHECK_NEW_TH(ptr, expression)

    This executes a new of the passed expression. In release mode, it handles the exception with a ML_PRINT_FATAL_ERROR post to the ML error manager and it throws either an ML_NO_MEMORY exception or an ML_CONSTRUCTOR_EXCEPTION dependent on whether the new statement returned NULL or the constructor threw an exception. The pointer must have been set to NULL before.

  3. ML_DELETE(ptr)

    This macro is used to delete an object allocated with ML_CHECK_NEW(ptr, expression) or with ML_CHECK_NEW_TH(ptr, expression). It must only be used with a single created object, not with an array (see below).

  4. ML_DELETE_ARRAY(ptr)

    This macro is used to delete an object allocated with ML_CHECK_NEW(ptr, expression[<objectNum>]) or with ML_CHECK_NEW_TH(). It must only be used for allocated object arrays.

[Important]Important

Always try to use the above macros for constructing and deleting objects inside of ML code. In future, this will provide a more powerful and failsafe memory management, and it will also correctly handle and log errors that occur in applications.

[Note]Note

See Section 2.2.2, “ Memory for an alternative memory management concept with ML allocation and freeing statements.

Validating Program States:

  1. ML_CHECK(<expression>)

    This macro posts an ML_PRINT_FATAL_ERROR() to the ML error manager if the passed <expression> returns false. This is the typical way of checking entry conditions in functions, for example.

    If the ML error manager continues execution, normal program execution continues after the ML_PRINT_FATAL_ERROR() macro.

  2. ML_CHECK_ONLY_IN_DEBUG(<expression>)

    This macro is identical with the ML_CHECK(<expression>) macro, however, it is only compiled in debug mode. In release mode, it is not implemented at all. So this macro is comparable to the normal assert() statement. With the assert() statement, however, errors are redirected to abort() and not to the ML error manager.

  3. ML_CHECK_THROW(<expression>)

    This macro posts an ML_PRINT_FATAL_ERROR() to the ML error manager if the passed <expression> returns false. This is the typical way of checking program or parameter states in functions for validity.

    If the ML error manager continues execution, this macro throws an ML_BAD_POINTER_OR_0 exception after the ML_PRINT_FATAL_ERROR() macro. Thus this macro is especially useful in code segments which are enclosed in ML_TRY { <function body> } ML_CATCH*() segments.

[Note]Note

Also see Section 5.2, “Handling Errors” for explicit usage of error and warning posts.

Example 5.9. Detailed Example for a Checked Object Allocation with ML_CHECK_NEW_THROW() and Release of Resources on Crashes

double MyClass::testFunction4()
{

  int *newArray = NULL;
  double retVal = 0;
  ML_TRY
  {
    // Allocate an integer array with new.
    ML_CHECK_NEW_THROW(newArray, int[200]);

    int result = 0;

    /*
     We assume that the function code makes use of the
     allocated data here and that it must calculate a
     non zero return value; if result remains 0 then
     we have a bug somwhere...
    */

    // This value is expected to be non zero, otherwise
    // we have a fatal error, check it chere.
    ML_CHECK_THROW(result);

    // Calculate the return value.
    retVal = 10. / result;

    // Release the allocated memory and reset pointer.
    ML_DELETE_ARRAY(newArray);
  }
  ML_CATCH_BLOCK(...){

    // Clean up allocated resources after any crash in
    // ML_TRY{ } block if pointer is non NULL.
    ML_DELETE_ARRAY(newArray);

    // Post and log the error.
    ML_CHECK(0);

    // Optionally and dependent on the way how the application
    // handles errors the exception can be propagated to the caller
    // such that it terminates execution until the main function is
    // reached and the program state is cleaned up correctly.
    // Another option would be to continue here.
    throw();
  }

  return retVal;
}