Skip to content

Comprehensive error framework for applications requiring functional and robust error handling, utilising the power of modern object-oriented Fortran.

License

Notifications You must be signed in to change notification settings

samharrison7/fortran-error-handler

Repository files navigation

Fortran Error Handler

DOI fair-software.eu

Fortran error handling frameworks are few and far between, and those that do exist often implement only parts of the error handling process, or rely on pre-processors. The goal of this error handling framework is to provide a universal and comprehensive solution for applications requiring functional and robust error handling, utilising the power of modern object-oriented Fortran.

Getting started

There are a few ways to get the Fortran Error Handler into your project. The following have been tested with recent version of the GFortran and Intel Fortran compiler (ifort, not ifx).

fpm - Fortran Package Manager

The simplest way is to use fpm (Fortran Package Manager). You can either directly include the Fortran Error Handler as a dependency in your fpm.toml file:

[dependencies]
feh = { git = "https://github.com/samharrison7/fortran-error-handler" }

Or you can clone the repo and build the library yourself using fpm:

$ git clone https://github.com/samharrison7/fortran-error-handler
$ cd fortran-error-handler
$ fpm build

A static library (e.g. libfeh.a on Linux) and .mod files will be generated in the build directory for you to use. An example executable (using example/example_usage.f90) will also be generated. Running fpm test will run tests (using tests/run_tests.f90) for the framework. You can also get fpm to install the Fortran Error Handler locally (e.g. to /home/<user>/.local):

$ fpm install

Fpm can easily be installed using Conda: conda install -c conda-forge fpm.

Grab the files

Another simple method is to simple grab a copy of the source files (in src/) and include at the start of your compilation setup. Source files should be compiled in this order: ErrorInstance.f90, ErrorHandler.f90, ErrorCriteria.f90, Result.f90. An example Makefile.example is included, which can be altered according to your compiler and preferences.

Meson

If you use meson, a meson.build file is provided. For example, if you want to build into the buildmeson directory:

$ meson buildmeson
$ ninja -C buildmeson

From meson 0.56, you can use the meson compile command instead of ninja. This will generate the example and test executables, a shared library and module files. You can run the tests directly using meson: meson test -C buildmeson.

By default, the library is built with a debug build type. To build for release (with -O3 optimisations), specify this via the --buildtype=release option:

$ meson buildrelease --buildtype=release
$ ninja -C buildrelease

Installing using meson (meson install) isn't recommended at the moment as .mod files are not installed - see this issue.

CMake

The code can also be compiled using CMake, which similarly generates a library and .mod files, an example executable, and executable of unit tests.

$ mkdir build
$ cd build
$ cmake ..
$ make
# To run the unit tests
$ ./test
# To run the example
$ ./example

Usage

Read the below documentation for example usage, and check out the example directory for ideas of how to incorporate into your project.

Basic structure

The framework consists of two main classes:

  • ErrorHandler: Responsible for initiating the error handling environment, queuing and triggering error events, and storing a list of possible error codes and their respective error messages.
  • ErrorInstance: An ErrorInstance is an object representing an error, containing an error code, error message, whether the error is critical (should stop the program executing), and a user-defined trace of where the error has come from.

A number of further classes provide added functionality:

  • Result: A Result object, though not required to use the framework, is designed as an object to be returned from any procedures that (may) throw an error. An object of type(Result) contains an errors component, which is an array of ErrorInstances and is intended to be returned from procedures that normally wouldn't return anything else (i.e., subroutines). To facilitate the returning of data as well as errors from procedures (i.e., functions), a number of separate types extend Result, each one dealing with data of a different rank (dimensionality). In addition to the errors component, they contain a data component, whose rank is determined by the type name. For example, type(Result0D) is used for scalar data (0D), type(Result1D) is used for data of rank-1 (1D) and type(Result2D) is used for data of rank-2 (2D). The maximum data rank is currently rank-4 (type(Result4D)).
  • ErrorCriteria: This class extends the ErrorHandler and defines a number of common "criteria" used for error checking, such as checking whether a number falls between given bounds. Criteria functions expedite the error checking process with intuitive function calls returning pre-defined ErrorInstances.

Quick start guide

Let's create a quick program that asks for a user's input, checks that it passes a number of criteria and if it doesn't, triggers an error.

Firstly, you need to include the appropriate modules to use:

use ErrorInstanceModule
use ErrorCriteriaModule
use ResultModule

ErrorCriteria extends ErrorHandler and so if we are using the ErrorCriteria class, we don't need to directly use the ErrorHandler. ErrorCriteria can be thought of as the ErrorHandler (thus responsible for storing and triggering errors), but with criteria-checking capabilities. If we're using it, we need to create and initialise an ErrorCriteria instance so that default ErrorInstances are set for different criteria. If we don't want criteria-checking capabilities, we can just create and initialise the ErrorHandler instead.

Let's also initialise an integer to store the user's input into, and a scalar (0D) Result object to store data and errors in:

type(ErrorCriteria) :: EH       ! If criteria-checking not needed, use ErrorHandler instead
integer :: i
type(Result0D) :: r

We first need to initialise the ErrorCriteria (and thus ErrorHandler). This sets a default error with code 1, and a "no error" with code 0, as well as a number of default errors for the criteria (see the ErrorCriteria docs). At the same time, let's specify two custom errors that we might want to trigger at some later point in the code. isCritical determines whether triggering of the error stops the program executing or just prints a warning, and it defaults to true (i.e., triggering an error stops the program).

call EH%init( &
    errors = [ &
        ErrorInstance(code=200, message="A custom error message.", isCritical=.false.), &
        ErrorInstance(code=300, message="Another custom error message.", isCritical=.true.) &
    ] &
)

We can also add errors to the ErrorHandler on the fly by using the add procedure: call EH%add(code=200, message="A custom error message.").

Now let's get the user to enter an integer, and specify that it should be between 0 and 10, but not equal to 5:

write(*,"(a)") "Enter an integer between 0 and 10, but not equal to 5:"
read(*,*) i

We now need to test i meets these criteria, and we can do that using the ErrorCriteria's limit and notEqual methods. We'll do this at the same time as creating the Result object, which stores the data i as well as any errors:

r = Result( &
    data = i, &
    errors = [ &
        EH%limit(i,0,10), &
        EH%notEqual(i,5) &
    ] &
)

Now we can attempt to trigger any errors that might be present, by using the ErrorHandler's trigger method, which accepts either an error code, an ErrorInstance or an array of ErrorInstances as its parameters. If the criteria check didn't result in any errors, nothing will happen (in reality, the criteria returns an ErrorInstance with code 0 - the default "no error" - which the trigger method ignores).

call EH%trigger(errors=r%getErrors())

The default ErrorInstances set up by the init procedure for ErrorCriteria all have isCritical set to true, and so if either of these criteria errors are triggered, program execution will be stopped. The default criteria errors can be altered by using the modify procedure - see the ErrorCriteria docs. Finally, let's print the value to the console:

write(*,"(a,i1)") "Input value is: ", .integer. r

A number of operators, such an .integer. and .real., are available to quickly return the data (which is stored polymorphically) from a Result object as a specific type. If you are uncomfortable using custom operators, then the corresponding procedures r%getDataAsInteger(), r%getDataAsReal(), etc, are also available. See the Result docs for more details.

Let's test it out with a few different integers:

$ Enter an integer between 0 and 10, but not equal to 5:
$ 12
$ Error: Value must be between 0 and 10. Given value: 12.
$ ERROR STOP 105

$ Enter an integer between 0 and 10, but not equal to 5:
$ 5
$ Error: Value must not be equal to 5. Given value: 5.
$ ERROR STOP 106

$ Enter an integer between 0 and 10, but not equal to 5:
$ 1
$ Input value is: 1

The stop codes 105 and 106 are the default error codes for those particular limit criteria. These codes can be modified using the ErrorCriteria's modifyErrorCriterionCode procedure. See the ErrorCriteria docs.

The framework is designed to work seemlessly in large object-oriented projects, where Result objects can be returned from functions with ErrorInstances that contain a user-defined trace of where the error came from. The goal of such a trace is to provide more useful errors to end users of your application, who might not have access to the source code and thus find standard stack traces containing references to files and line numbers useless. More details can be found in the ErrorInstance docs, and more thorough examples in the examples directory.

Learn more

Explore the documentation for each class to learn how to best use the framework, and browse the examples to get an idea of how to implement the framework into your project:

Caveats and limitations

  • Error code must be less than 99999.
  • Result objects only support up to rank-4 (4 dimensional) data.
  • Limited support for different kinds, due to Fortran's lack of kind polymorphism. In particular, ErrorCriteria only accept 4-byte integers and single precision, double precision and quadruple precision reals, as such:
integer, parameter :: dp = selected_real_kind(15,307)
integer, parameter :: qp = selected_real_kind(33,4931)

integer :: i = 1
real :: r = 1.0
real(dp) :: r_dp = 1.0_dp
real(qp) :: r_qp = 1.0_qp

About

Comprehensive error framework for applications requiring functional and robust error handling, utilising the power of modern object-oriented Fortran.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages