The Fortran language specification does not provide the means to do exception handling in the most cases. For some routines such as the I/O functions, it is possible to implement separate error handlers which basically act in a goto
manner. However, there is no possibility to propagate exceptions at different levels or to implement memory protection blocks in a comfortable way.
Using features introduced in Fortran 2003 and 2008, together with routines from an external C++/Assembler library, all this becomes possible. The Fortran Exception Library enables the programmer to use the common try-except-finally syntax known from most major languages.
At the moment, this library is available only for Intel Fortran. It was tested with ifort 16.0.4
on Linux and with ifort 17.0.2
on Windows, both on x86-32 and x86-64 architecture. The C++ compiler used was icc
in the same version.
If you are a gfortran
user, you are welcome to contribute a port for this compiler. It is not clear whether the same functionality is achievable, but see the implementation section for details.
program TestExceptions
# include <Exceptions.FPP>
implicit none
try
call someBadSubroutine()
print *, "Never here"
except(E)
type is(ENotSupportedException)
print *, "Got an exception: ", E%Message
endExcept
contains
subroutine someBadSubroutine()
throw(ENotSupportedException("This is not supported."))
end subroutine
end program
Also see the examples in the test case.
Some pre-compiled versions are available in the release section; however, you may also compile the library on your own.
Open the solution Exceptions.sln
in Visual Studio. Ensure you choose the Release
configuration (unless you want to debug the library) and select the appropriate platform, x64
or x86
. Compile the complete solution; dependencies are already set up such that the compilation will happen in the correct order. You may also want to use the batch build to create all configurations. To check that everything worked well, run ExceptionTester
and make sure all tests are passed.
Make sure the icc
and ifort
compiler are known. Move to src/ExceptionHandler
and compile the C++ handler:
[ExceptionHandler]$ icc -O3 -std=c++11 -fasm-blocks -c -o ../../lib/<arch>/ExceptionHandler.o CExceptions.cpp
Here, <arch>
shogitgituld be replaced with either x86-64
or x86-32
, depending on your platform target and the compiler used. Make sure the paths all exist!
After that, compile the Fortran library. Move to src/Exceptions
.
[Exceptions]$ ifort -O3 -fpp -w -I../../include -module ../../modules/<arch> \
-c mLogger.f90 mExceptionClasses.F90 mException.F90
[Exceptions]$ mv *.o ../../lib/<arch>
Finally, we will create a static library out of those. Move to lib/<arch>
.
[<arch>]$ xiar rc libExceptions.a *.o
The Fortran part of the library consists of two modules and a set of preprocessor directives. They are altogether loaded by the preprocessor statement
#include <Exceptions.FPP>
This expression has to be put at a position where otherwise, a use
statement would be put, so at the beginning of a module, program, subroutine or function. It is not a problem if there are several modules in the same file which all need to include this file.
After this line, all the features described below are available and ready for use.
Several options have to be set in the project properties.
- The preprocessor needs to be turned on (Fortran > Preprocessor > Preprocess Source File; all configurations and platforms).
- The preprocessor and the compiler have to be aware of the include directory which contains the
FPP
files. Add theinclude/
directory (Fortran > General or Preprocessor > Additional Include Directories; all configurations and platforms). - The same holds true for the module files of the Fortran part. Since these are architecture specific,
modules/<arch>
needs to be added to the include directory as well. - The library has to be provided to the linker. Since you typically do not want to have the exception projects in your solution (in which case you only needed to set the dependency on
Exceptions
and also didn't have to do the previous step), you need to specify the complete path of thelibExceptions.lib
file located in thelib/<arch>
directory directly by adding it as explicit dependencies (Linker > Input > Additional Dependencies). TheExceptionHandler.lib
dependency is not needed, since it is automatically linked together with theExceptions
project intolibExceptions.lib
.
It is recommended to add the /w
option to the command line of the compiler; for else, the preprocessor would issue lots of warnings (see implementation notes).
To simplify the commands, a module file is provided; when put in the appropriate module directory, load the module via module load fortException
. Before this, make sure that you provide the correct absolute path to the exception library root directory in the module file. Then, the required include paths for the preprocessor as well as the library path are automatically set. You then only have to specify that the program should also be linked against the library, i.e.
$ ifort -lstdc++ -fpp -w <arguments such as output and source files> -lExceptions
This makes sure the standard C++ library is included together with the exceptions library itself (which has to come after the last file which uses exceptions, so best to append it to the command line). The preprocessor is turned on by -fpp
and preprocessor warnings are suppressed by -w
.
-
Initialization
The C++ and the Fortran part of the library are tightly connected together. At the beginning of the program, some Fortran startup code is called1. Apart from initialization of a few variables, thesignalQQ
and most importantly, theestablishQQ
function are called. These are non-standard extensions of Intel Visual Fortran and allow to implement custom error handlers when the system or the runtime library detects a problem. By doing so, errors which are normally fatal (such as integer division by zero) are converted into ordinary exception objects and can be caught in the application.
1 Since Fortran does not support initialization functions, this call is invoked from a C++ global constructor. -
Starting the protected block
When the preprocessor encounters thetry
statement, it calls the C++ handler routine. The current CPU status (all registers which are required by the calling convention, possibly also FPU registers and control word) is stored in a stack which is thread-local. Thetry
statement starts thetry
block, which has to be followed by one handler. If you want to use bothexcept
andfinally
, you may use the shorthandtry2
, which just translates into two nested try blocks. -
Throwing an exception
In case an exception is thrown, the C++ handler retrieves the last saved state on the stack and basically performs a longjump. Execution will continue at thetry
statement, which then jumps over thetry
block into the handler following. At the same time, the Fortran layer has set a thread-global pointer to the exception object which was passed tothrow
.
The syntax isthrow(<ExceptionClass>([Message], [Code]))
, where<ExceptionClass>
is any of the predefined or a self-defined2 exception class. Both the stringMessage
and the integerCode
are optional. The function which is invoked by issuing the class name of the exception creates a new exception object on the heap and copies the message string into. It returns a pointer to the object. If you ever use these functions outside of thethrow
directive, it is your responsibility to free the allocated memory afterwards. Else, the framework takes care of releasing the memory as soon as the object is no longer needed - so don't pass exception objects pointers tothrow
which you expect to be valid after the exception had been caught.
2 There are convenience macros provided for creating a new exception class. To see some examples, refer to themExceptionClasses.F90
. Basically, you need to issuedeclareException(<NewClass>, <ParentClass>)
in the declaration section andimplementException(<NewClass>)
in thecontains
section. -
Except
By issuingexcept(E)
- whereE
can be any valid variable name -, the beginning of a handler block is declared. The current exception object will be retrieved by the library and assigned to the variableE
, which only exists within this block.except
must be treated as aselect type(E)
statement, which means that the very first statement in the block needs to betype is
orclass is
.class default
is prohibited; in case you want to catch all exceptions, simply useclass is(Exception)
, sinceException
is the base class for all exceptions. Theexcept
block will be skipped if no exception was thrown in its precedingtry
block (at any level of nesting). If an exception was caught in theexcept
block - so there was a matching type selector -, then the exception object itself is deallocated and execution continues after the block as if there were no exception. If no matching type selector was found or the commandraise
was executed in a matching type selector3, the exception is re-thrown and higher-lying handler blocks can handle the error.
The block is ended byendExcept
.
3 Note that you cannot usethrow(E)
, sinceE
is a construct association and not a pointer and hence cannot be passed tothrow
. -
Finally
By issuingfinally
, the beginning of a handler is declared. This handler will always be executed after thetry
block, regardless of what happened inside. Therefore, the exception object (if one was thrown at all) is not available within this block. If an exception was thrown, it will be re-raised after the execution of this block.
The block is ended byendFinally
. -
Control flow interrupting statements
The usual sequential execution of a program can be altered in Fortran by several means. Thereturn
statement jumps to the end of the current execution unit,exit
terminates a loop (or a named block),cycle
skips the rest of the current loop iteration and continues with the next and if you ever really need it,goto
allows to directly jump to labeled positions4. The exception library tries a lot so that these instructions can be used even within blocks while upholding the condition thatfinally
is always executed. This means that if any of those statements is executed within atry
block with the effect that the rest of the block is skipped (jumps within the block work just in the normal way), thefinally
block will be executed first; after that, the jump produced by these statements is carried out.
This is implemented by overriding the keywords listed above with the preprocessor. Basically, a call to the C++ library is performed just before the jump statement. This call is responsible for storing the address of the jump instruction to the last state on the exception stack.5
When the jump is then carried out, thetry
block is left. Internally it is implemented as ablock
which - apart from the aforementioned constant - also holds a static finalizable helper object. Hence, the compiler invokes the finalization routine, which calls the C++ library. The library notices that the block was left in an anomalous way and performs a longjump to the handler.
Due to the longjump, the CPU state is restored as it was when thetry
was first executed, only that now thetry
block is skipped and the handler is executed6.
At the end of the handler, the C++ library is once again invoked and jumps back to thereturn
/exit
/cycle
/goto
statement, which is executed once again. Note that this jump restores the CPU state as it was when the jump was about to be performed before. However, due to the execution of the handler, the stack was decremented and incremented in between. Hence, the stack content might be cluttered and any local stack variable which was not yet known in the outertry
block7 might be invalid. Luckily, this does not matter anymore8 and the next execution of the jump command triggers our finalization handler again. But it knows that it was already executed and does nothing. This propagates through any level of nested blocks.
The essence of the previous description: The library takes care of any jump commands in a highly non-trivial manner. Don't introduce any finalizable variables (and allocatables also need finalization) on a scope which is only valid within a part of atry
block (i.e. don't use Fortranblock
s with finalizable variables when you do a jump out of such a block). The library overwrites compiler instructions to achieve this; hence, the preprocessor will issue lots of warnings. It is recommended to use the/w
(Windows) or-w
parameter in order to disable these warnings.
What else should be avoided?- Computed
goto
statements allow jumping to one out of several destinations, depending on a certain criterion. Don't use them.
As described above, the jump statement needs to be evaluated several times. As long as the computation does not depend on block-local user-introduced variables (regardless of whether they are finalizable or not), this should in principle work. But it is a waste of time to compute the destination several times. Just useif
constructs (and not arithmeticif
s!), so that the call to store the state can be injected to a place where the computation was already carried out. - Do not use alternate returns that lead out of a block.
The library has no means to detect alternate returns (which are obsolescent anyway). The same holds true for branch specifiers (as inopen
,allocate
). Branch targets within the block are perfectly fine. If an alternate return or branch specifier jumps out of a block, the library is left in an inconsistent state, since leaving the block did not pop an element from the state stack. Hence, the outcome will be completely unpredicable, together with memory leaks occurring for sure. - If you want to execute one of the jump statements above, but you know for sure that it is not and will never be contained (at execution unit level) by a
try
,except
orfinally
block, you may want to bypass the substitution of the preprocessor. This is possible, as the preprocessor is case-sensitive but Fortran is not. All preprocessor defines of the exception library are given in lower case, camel case (if multiple words are joined), with a starting upper case letter, the same for camel case and all in upper case. Therefore, just use some weird capitalization to access the original statement. Recommended is changing the last letter, e.g.returN
if you write in lower case orRETURn
if you write in upper case. You must not use this spelling inside a block if it the jump goes outside.
4 Note that it is not possible to jump into a
try
,except
orfinally
block from outside.
5 Note that this code is also generated if the statements are not within atry
block, since the preprocessor cannot distinguish them; however, with optimization turned on, the compiler should remove this call - it is conditionally executed only if a constant is.true.
. This constant is defined in the Fortran library as.false.
, but it is covered by a block-local constant defined as.true.
.
6 Internally, the handlers operate in yet anothertry
level, such that exceptions and jumps within are noticed.
7 This can happen if the programmer introduced yet another ordinary Fortranblock
with variables.
8 Unless this programmer-introduced innerblock
contains a finalizable variable, which thus was already finalized when first leaving the block and now can contain anything. This will cause a mess. Don't do it. - Computed
-
Default error handler
When an exception is not caught, the default error handler is invoked. It prints the class name9 of the exception together with its code and message as well as a traceback.
Note that at least inifort 17
, there is a bug in thetracebackQQ
subroutine. If your application uses thecomplex
type of any kind at any place, the traceback function causes a crash. A bug report is submitted, but not cared for by Intel at the moment. You probably want to comment out calling the traceback inmException.F90
, subroutineException$defaultHandler
. The program execution stops immediately, though first the library finalizes.
9 Obtaining the class name of an object is not possible in standard Fortran, nor does Intel Fortran provide a non-standard extension for this. Hence, the functiongetClassName
relies on the compiler-internal structure, which might change at any time whithout any notice. At the moment, the implementation seems to work forifort 16
andifort 17
, where separate offsets are provided for the x86-32 and x86-64 version; however, on both Windows and Linux, the structure looks like the same. Therefore, it is crucial that this function is tested against every new version. -
Assertions
The library provides the macroassert(condition)
, which raises anEAssertionFailed
exception in casecondition
is.false.
. The message of the exception object contains the file name and line in which the assertion was triggered. Assertions are only activated when the symbol_DEBUG
is defined; in release mode, they just evaluate tocontinue
(which makes them available as branch targets in both configurations). -
Finalization
At the end of the program, the variables from the beginning are deallocated. This will always be done upon normal termination or when an exception ended the program. However, if you issue any of thestop
,error stop
commands or one of the non-standard extensions which all do the same, the finalization cannot be performed. In this case, if you scan for memory leaks, you will find an expected leakage of 10 strings. Anything different from this which originates in the library is a bug and should be reported. -
Thread-safety
The library is thread-safe, since the global information in both C++ and Fortran are stored in thread-local variables. In C++, this is achieved via thethread_local
statement; Fortran uses the!$OMP THREADPRIVATE(...)
directive. Hence, the Fortran version is not really thread-safe if you launch multiple threads via API functions instead of OpenMPI. This will put the Fortran and C++ backend in an inconsistent state and therefore must not be done.
It is always possible to use multiple threads if the main thread is the only one which ever uses the exception framework.
Due to the fact that the programmer does not have to write any code in order to launch multiple threads and for example parallelize loops or assignments, the behavior of the library might be somewhat unexpected, though it is perfectly understandable if one thinks about the fact that every thread in most programming language (unless some threading library is used) is a separate worker which should have its own exception handling, where the thread procedure would not be implemented in the same place as the initializing main thread. However, consider the following scenario:try !$OMP PARALLEL DO do I = 1, 10 if(I == 5) then throw(Exception("What now?")) end if end do !$OMP END PARALLEL DO except class is(Exception) print *, "Something went wrong" endExcept
One of the worker threads will throw an exception; but since the handler was declared in the main thread, the worker does not know of it. It therefore encounters an unhandled exception and will cause the program to stop. Of course, by putting the
try
-except
block inside the loop, this problem would not arise. But then, this would be extremely inefficient for large loops, since the protection schemes do cost some time. The behavior one typically wishes is this: Execute the loop in parallel, but when an exception occurs, terminate all workers and handle the problem in the main thread. Perhaps this is something that can be done; feel free to contribute. -
Performance
On the one hand, any kind of exception handling will slow down the program. On the other hand, the programming logic can greatly benefit from the use of a proper structured exception handling. And of course, the development process can also speed up a lot if the developer has powerful tools for the error analysis at hand (for example, the usual segmentation fault should now rarely occur and instead translate to a understandable and in most cases also catchable exception; the same holds e.g. for division by zero). It is therefore important to know which statements have an impact on performance and which don't.
You may throw an exception out of any procedure which carries out CPU intensive calculation without needing to fear any consequences (unless, of course, you optimize for a specific microcontroller and the throw statement is just too large to fit in the µop-cache; but typically, pure Fortran cannot be optimized to such a degree). This of course assumes that you use exceptions for exceptional cases - so when you want the calculation to be aborted by the exception. If you just throw an exception with every iteration of the innermost loop, then there is no help.
You will be able to notice a performance impact if you open error handler blocks within a loop. The blocks involve calling C++ functions which are probably located far away and will therefore purge the code cache. Additionally, the code has to access thread-local variables, which is inefficient in itself. This should not prevent you from using the exception library, as it won't have a noticable impact unless it is repeated thousands of times.