PWR068: Encapsulate procedures within modules to avoid the risks of calling implicit interfaces
Issue
Calling a procedure without an explicit interface prevents the compiler from verifying argument compatibility, increasing the risk of difficult-to-diagnose runtime bugs.
Actions
To enhance code safety and reliability, encapsulate procedures within modules to automatically provide an explicit interface at the point of the call.
Relevance
Fortran allows procedures to be called without explicit information about the number of expected arguments, order, or properties such as their type. In such cases, an implicit interface is used. The caller simply provides a list of memory addresses, which the called procedure assumes point to variables matching the dummy arguments. This can easily lead to issues such as:
-
Type mismatches: Passing variables of one type (e.g.,
real
) as another type (e.g.,integer
) causes errors due to different internal representations. -
Missing arguments:
- Input arguments: Omitted arguments are initialized to undefined values, resulting in unpredictable behavior.
- Output arguments: Writing omitted arguments results in invalid memory accesses, potentially crashing the program.
In contrast, a procedure with an explicit interface informs the compiler about the expected arguments, allowing it to perform the necessary checks at the point of the call during compile-time. The preferred approach to ensure a procedure has an explicit interface is to encapsulate it within a module, as illustrated below.
Code examples
The following program calculates the factorial of a number. To simulate a real project with multiple source files, the main program and the factorial procedure are in different files:
! example_factorial.f90
subroutine factorial(number, result)
implicit none
integer, intent(in) :: number
integer, intent(out) :: result
integer :: i
result = 1
do i = 1, number
result = result * i
end do
end subroutine factorial
! example.f90
program test_implicit_interface
implicit none
external :: factorial
real :: number, result
number = 5
call factorial(number, result)
print *, "Factorial of", number, "is", result
end program test_implicit_interface
You may have already noticed that the main program incorrectly assumes that the
factorial
subroutine uses real
variables, instead of integer
. Indeed,
running the program produces an incorrect result:
$ gfortran --version
GNU Fortran (Debian 12.2.0-14) 12.2.0
$ gfortran example_factorial.f90 example.f90
$ ./a.out
Factorial of 5.00000000 is 0.00000000
The compiler cannot catch this bug during compilation because the called
procedure has an implicit interface: it is an external
element defined in
another source file.
A simple solution is to encapsulate the procedure within a module. This informs the compiler about the exact location where the called subroutine is defined, enabling it to verify the provided arguments against the actual dummy arguments.
Moving the factorial
subroutine to a module is as simple as:
! solution_mod_factorial.f90
module mod_factorial
implicit none
contains
subroutine factorial(number, result)
implicit none
integer, intent(in) :: number
integer, intent(out) :: result
integer :: i
result = 1
do i = 1, number
result = result * i
end do
end subroutine factorial
end module mod_factorial
! solution_with_type_mismatch.f90
program test_explicit_interface
use mod_factorial, only: factorial
implicit none
real :: number, result
number = 5
call factorial(number, result)
print *, "Factorial of", number, "is", result
end program test_explicit_interface
With the explicit interface, the compiler catches the argument mismatches at compile-time, avoiding the runtime bug:
$ gfortran solution_mod_factorial.f90 solution_with_type_mismatch.f90
solution_with_type_mismatch.f90:7:32:
7 | call factorial(number, result)
| 1
Error: Type mismatch in argument ‘number’ at (1); passed REAL(4) to INTEGER(4)
solution_with_type_mismatch.f90:7:32:
7 | call factorial(number, result)
| 1
Error: Type mismatch in argument ‘result’ at (1); passed REAL(4) to INTEGER(4)
Once the appropriate integer
types are used, the program compiles and
produces the correct result:
$ gfortran solution_mod_factorial.f90 solution.f90
$ ./a.out
Factorial of 5 is 120
The previous example demonstrates how calls to external
procedures are
performed through implicit interfaces. The same problem would occur if
factorial
were an implicitly declared procedure, as shown in the following
example:
program test_implicit_interface
real :: number, result
number = 5
call factorial(number, result)
print *, "Factorial of", number, "is", result
end program test_implicit_interface
Note the absence of implicit none
, allowing the symbol factorial
to be
interpreted as an implicitly declared entity.
It's possible to manually define explicit interfaces using the interface
construct at the call site. However, this approach introduces risks. The
procedure's definition must be duplicated, but there's no mechanism to ensure
this replica matches the actual definition of the original procedure, which
can easily lead to errors:
program test_implicit_interface
implicit none
interface
subroutine factorial(number, result)
real, intent(in) :: number
real, intent(out) :: result
end subroutine factorial
end interface
real :: number, result
number = 5
call factorial(number, result)
print *, "Factorial of", number, "is", result
end program test_implicit_interface
In this example, the manually declared interface for factorial
is
incorrect; the dummy arguments are declared as real
instead of integer
.
This error won't be caught at compile time, and will still result in an
unexpected output during execution.
When interoperating between Fortran and C/C++, it's necessary to manually define explicit interfaces for the C/C++ procedures to call. Although this is not a perfect solution, since the are no guarantees that these interfaces will match the actual C/C++ procedures, it's still best to make the interfaces as explicit as possible. This includes specifying details such as argument intents, to help the Fortran compiler catch early as many issues as possible.
If modifying legacy code is not feasible, create a module procedure that wraps the legacy procedure as an indirect approach to ensure argument compatibility.
Related resources
References
-
"Implicit Interfaces", Adrian Sandu. [last checked May 2024]
-
"More on Implicit Interfaces", Adrian Sandu. [last checked May 2024]
-
"Explicit Interfaces", Adrian Sandu. [last checked May 2024]
-
"Doctor Fortran Gets Explicit!", Steve Lionel. [last checked May 2024]
-
"Doctor Fortran Gets Explicit - Again! ", Steve Lionel. [last checked May 2024]
-
"Fortran code modernization", Reinhold Bader. [last checked May 2024]