Fortran calling Rust

A tour of interfacing Fortran with Rust through a C interface

March 23, 2025


Screenshot of an early draft which looks like written on typewriter

Screenshot of an early draft. Maybe writing it on a typewriter would have made the blog post shorter. This image is created using the wonderful OverType.

Why Fortran and Rust?

There are many things I like about modern Fortran (e.g. the simplicity, how it helps to write modular code, support for pure functions). What I miss in modern Fortran are data structures like a hash table (dictionary) or a vector/list (dynamic array) or a set. These are standard in many other languages.

Often I feel there is too much focus on potential speed when comparing languages.

Neither Rust nor Fortran guarantee that your code will be fast. It is possible to write slow or unmaintainable code in any language.

What I like about Rust is the tooling, the type safety, the memory safety, thread safety, and the ecosystem. Rust code can also be really fast and so can Fortran code.

In one of my projects I needed to add some functionality to a 200k lines Fortran code. The thing I needed was possible but not trivial to do in Fortran (I needed something resembling a priority queue). I did not want to implement the basic data structures in Fortran myself or complicate the code by emulating them using arrays. I chose to write the new part in Rust and call it from Fortran.

Fortran is very popular in the scientific community (especially in computational physics and chemistry, in geosciences, and weather forecasting) and it is here to stay for decades to come. I believe that there will be many situations where the solution is not to rewrite 1M lines of Fortran code but to couple these two fine languages. Below I describe how I approach this.

How?

Rust has excellent interoperability with C. Fortran has excellent interoperability with C as well (via iso_c_binding). This means that a good medium for Fortran to talk to Rust is via a C interface. We will see that we can make the C interface relatively "thin" (perhaps 20 more lines of code to write?).

Functions which return a single value

Let's start with a relatively simple example. On the Rust side I will create few functions that can add numbers and return a single value. On the Fortran side I will call these functions.

You might prefer to compile the Rust code into build/ instead of rust/target/. Further below I will show how that can be done.

All examples in this post will use this directory structure:

.
├── build            <- the build directory
├── CMakeLists.txt
├── example.f90      <- the Fortran code
├── interface.f90    <- here we explain the interface to Fortran
└── rust
    ├── Cargo.lock
    ├── Cargo.toml
    ├── src
    │   └── lib.rs   <- the Rust code
    └── target       <- the default place where Rust compiles to

Let's start with the Rust code (rust/src/lib.rs). The first two functions sum two numbers. The third function sums the elements of an array:

#[unsafe(no_mangle)]
pub extern "C" fn sum_two_integers(a: i32, b: i32) -> i32 {
    a + b
}

#[unsafe(no_mangle)]
pub extern "C" fn sum_two_doubles(a: f64, b: f64) -> f64 {
    a + b
}

/// # Safety
///
/// Function assumes that the input data is a valid pointer to an array and that the
/// array size matches the `size` parameter.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sum_integer_array(data: *const i32, size: usize) -> i32 {
    if data.is_null() {
        return 0;
    }

    let slice = unsafe { std::slice::from_raw_parts(data, size) };

    slice.iter().sum()
}

Rust does not need to know about the Fortran side. We ask it to create C-compatible interfaces with the pub extern "C" and #[unsafe(no_mangle)] attributes.

We should also inspect the Cargo.toml file. The highlighted line is important to get a C-compatible dynamic library:

[package]
name = "rust"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]

Fortran does not know that it will be calling Rust. It thinks it is talking to C.

I am not sure about the highlighted line. Maybe c_size_t would be more correct here but then I would have to use interger(8) on the Fortran side.

Now let us look at the Fortran interface file (interface.f90):

module rust_interface

    use, intrinsic :: iso_c_binding, only: c_int32_t, c_double

    implicit none

    interface
        function sum_two_integers(a, b) result(output) bind(c)
            import :: c_int32_t
            integer(c_int32_t), intent(in), value :: a
            integer(c_int32_t), intent(in), value :: b
            integer(c_int32_t) :: output
        end function

        function sum_two_doubles(a, b) result(output) bind(c)
            import :: c_double
            real(c_double), intent(in), value :: a
            real(c_double), intent(in), value :: b
            real(c_double) :: output
        end function

        function sum_integer_array(data, size) result(output) bind(c)
            import :: c_int32_t
            integer(c_int32_t), intent(in) :: data(*)
            integer(c_int32_t), intent(in), value :: size
            integer(c_int32_t) :: output
        end function
    end interface

end module

Here is example.f90 which uses the Rust code via the interface:

program example

    use rust_interface

    implicit none

    integer, allocatable :: integer_array_1d(:)
    integer, allocatable :: integer_array_2d(:, :)


    print *, "sum of two integers:", sum_two_integers(2, 3)
    print *, "sum of two doubles:", sum_two_doubles(2.0d0, 3.0d0)


    allocate(integer_array_1d(5))
    integer_array_1d = [1, 2, 3, 4, 5]
    print *, "sum of 1D array:", &
            sum_integer_array(integer_array_1d, size(integer_array_1d))
    deallocate(integer_array_1d)


    allocate(integer_array_2d(2, 3))
    integer_array_2d = 1
    print *, "sum of 2D array:", &
            sum_integer_array(integer_array_2d, size(integer_array_2d))
    deallocate(integer_array_2d)

end program

Finally, the CMakeLists.txt file:

cmake_minimum_required(VERSION 3.10)

project("example" LANGUAGES Fortran)

add_executable(example "example.f90")

target_sources(
  example
  PRIVATE
    "interface.f90"
  )

target_link_libraries(
  example
  PRIVATE
    "${CMAKE_CURRENT_SOURCE_DIR}/rust/target/release/librust.so"
  )

I compile the Rust part separately:

$ cd rust
$ cargo build --release
$ cd ..

Yes, there is a more portable way to configure and build a CMake project but my muscle memory is trained to build this way.

This is how I compile the Fortran part:

$ mkdir build
$ cd build
$ cmake ..
$ make

This is the output I got. Seems to be working:

sum of two integers:           5
sum of two doubles:   5.0000000000000000
sum of 1D array:          15
sum of 2D array:           6

Do we need to worry about the C naming convention?

We don't have to worry about the C naming convention and name mangling. The extern "C" attribute creates a C-compatible interface. The no_mangle attribute tells Rust not to mangle (modify) the name of the function and make it findable by the linker under this name (sum_integer_array):

#[unsafe(no_mangle)]
pub unsafe extern "C" fn sum_integer_array(data: *const i32, size: usize) -> i32 {
    // ...
}

In this case both languages use the same name. It is possible to bind a Fortran interface to a C function (exported from Rust) with a different name.

On the Fortran side we used bind(c) to bind this to a C function with the same name:

function sum_integer_array(data, size) result(output) bind(c)
    ! ...

How about the unsafe keyword? Is my code unsafe now? This just means that we are leaving the jurisdiction of the Rust borrow checker and it is up to us to use this function in a responsible way.

Should we not compile the Rust library as part of CMake?

It is possible to instruct CMake to compile the Rust library as part of the build process and do everything in one go.

I prefer to do this separately. It reduces the complexity of CMakeLists.txt and it gives me the flexibility to iterate on the Rust code without having to recompile/relink the whole project every time I change a tiny thing on the Rust side since the Rust code is dynamically linked.

Working with arrays

Here we will take it one step further and modify arrays on the Rust side.

The allocation and deallocation of memory is managed by Fortran. It would be possible to allocate Rust-side and return a pointer to Fortran but I find it easier to let Fortran manage the memory for arrays used in Fortran.

This is the Rust code (rust/src/lib.rs):

/// # Safety
///
/// Function assumes that the input data is a valid pointer to an array and that the
/// array size matches the `size` parameter.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sum_integer_array(data: *const i32, size: usize) -> i32 {
    if data.is_null() {
        return 0;
    }

    let slice = unsafe { std::slice::from_raw_parts(data, size) };

    slice.iter().sum()
}

/// # Safety
///
/// Function assumes that the input data is a valid pointer to an array and that the
/// array size matches the `size` parameter.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn modify_array(data: *mut i32, size: usize) {
    if data.is_null() {
        return;
    }

    let slice = unsafe { std::slice::from_raw_parts_mut(data, size) };

    for element in slice {
        *element *= 2;
    }
}

/// # Safety
///
/// Function assumes that the input data is a valid pointer to an array and that the
/// array size matches the `size` parameter.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn check_array_order(data: *const f64, size: usize) {
    if data.is_null() {
        return;
    }

    let slice = unsafe { std::slice::from_raw_parts(data, size) };

    let n = size / 3;
    let mut result = vec![[0.0; 3]; n];

    // copy values from fortran-order into row-major representation
    for i in 0..n {
        result[i][0] = slice[i];
        result[i][1] = slice[i + n];
        result[i][2] = slice[i + 2 * n];
    }

    dbg!(result);
}

No surprises here.

This is the interface (interface.f90):

module rust_interface

    use, intrinsic :: iso_c_binding, only: c_int32_t, c_double

    implicit none

    interface
        function sum_integer_array(data, size) result(output) bind(c)
            import :: c_int32_t
            integer(c_int32_t), intent(in) :: data(*)
            integer(c_int32_t), intent(in), value :: size
            integer(c_int32_t) :: output
        end function

        subroutine modify_array(data, size) bind(c)
            import :: c_int32_t
            integer(c_int32_t), intent(inout) :: data(*)
            integer(c_int32_t), intent(in), value :: size
        end subroutine

        subroutine check_array_order(data, size) bind(c)
            import :: c_double, c_int32_t
            real(c_double), intent(in) :: data(*)
            integer(c_int32_t), intent(in), value :: size
        end subroutine
    end interface

end module

The two interesting calls are highlighted.

Finally, the Fortran code (example.f90):

program example

    use rust_interface

    implicit none

    integer, allocatable :: integer_array_1d(:)
    integer, allocatable :: integer_array_2d(:, :)
    real(8), allocatable :: array_doubles(:, :)


    allocate(integer_array_1d(5))
    integer_array_1d = [1, 2, 3, 4, 5]

    print *, "sum of 1D array:", &
            sum_integer_array(integer_array_1d, size(integer_array_1d))

    call modify_array(integer_array_1d, size(integer_array_1d))

    print *, "after modification:", &
            sum_integer_array(integer_array_1d, size(integer_array_1d))

    deallocate(integer_array_1d)


    allocate(array_doubles(2, 3))

    array_doubles(1, 1) = 1.0d0
    array_doubles(1, 2) = 2.0d0
    array_doubles(1, 3) = 3.0d0
    array_doubles(2, 1) = 4.0d0
    array_doubles(2, 2) = 5.0d0
    array_doubles(2, 3) = 6.0d0

    call check_array_order(array_doubles, size(array_doubles))

    deallocate(array_doubles)

end program

To get the same order of elements in the 2D array on the Rust and Fortran side, I had to reorder the elements in the Rust code:

sum of 1D array:          15
after modification:          30

[src/lib.rs:55:5] result = [
    [
        1.0,
        2.0,
        3.0,
    ],
    [
        4.0,
        5.0,
        6.0,
    ],
]

Maintaining a context on the Rust side

In my real-world case I wanted to initialize a priority queue once and then query it a million times.

In the next example I wanted to initialize/allocate some data in Rust once, then query the code many times, and finalize/deallocate at the end:

program example

    use rust_interface

    implicit none

    call initialize()

    print *, "result of query:", query()
    print *, "result of query:", query()
    print *, "result of query:", query()
    print *, "result of query:", query()
    print *, "result of query:", query()

    call finalize()

end program

This is the interface (interface.f90):

module rust_interface

    use, intrinsic :: iso_c_binding, only: c_int32_t

    implicit none

    interface
        subroutine initialize() bind(c)
        end subroutine

        function query() result(output) bind(c)
            import :: c_int32_t
            integer(c_int32_t) :: output
        end function

        subroutine finalize() bind(c)
        end subroutine
    end interface

end module

On the Rust side I achieved this by using once_cell crate which will hold the context/state across the queries:

[package]
name = "rust"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
once_cell = "1.21.1"

The struct State holds the context/state and the code makes sure that I can only have one instance of it at a time.

Later I will show examples where we can maintain multiple contexts.

This is the Rust code (rust/src/lib.rs):

use once_cell::sync::Lazy;
use std::sync::Mutex;

static GLOBAL_STATE: Lazy<Mutex<Option<State>>> =
    Lazy::new(|| Mutex::new(Some(State { value: 0 })));

struct State {
    value: i32,
}

#[unsafe(no_mangle)]
pub extern "C" fn initialize() {
    let mut state = GLOBAL_STATE.lock().unwrap();
    *state = Some(State { value: 42 });
    println!("initialized");
}

#[unsafe(no_mangle)]
pub extern "C" fn query() -> i32 {
    let state = GLOBAL_STATE.lock().unwrap();

    match state.as_ref() {
        Some(s) => s.value,
        None => {
            println!("query failed: state is uninitialized");
            -1 // return an error code or default value
        }
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn finalize() {
    let mut state = GLOBAL_STATE.lock().unwrap();
    *state = None; // drop the data
    println!("finalized and deallocated");
}

Example: Using Rust's HashMap in Fortran

In the next example I want to be able to allocate and hold some data structures on the Rust side, and then query and modify them from Fortran. And I want to be able to have more than one instance of these data structures.

HashMap is one of those data structures that I miss most in Fortran.

In the Rust code example I was lazy writing the actual safety notices.

The example data structure will be a HashMap but we can imagine something more involved (rust/src/lib.rs):

use std::collections::HashMap;

#[unsafe(no_mangle)]
pub extern "C" fn create_map() -> *mut HashMap<i32, i32> {
    Box::into_raw(Box::new(HashMap::new()))
}

/// # Safety
///
/// Safety notice needed here.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn insert_into_map(map_ptr: *mut HashMap<i32, i32>, key: i32, value: i32) {
    if map_ptr.is_null() {
        return;
    }
    let map = unsafe { &mut *map_ptr };
    map.insert(key, value);
}

/// # Safety
///
/// Safety notice needed here.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn get_from_map(map_ptr: *mut HashMap<i32, i32>, key: i32) -> i32 {
    if map_ptr.is_null() {
        return -1; // return -1 if the map is null
    }
    let map = unsafe { &mut *map_ptr };
    *map.get(&key).unwrap_or(&-1) // return -1 if key is not found
}

/// # Safety
///
/// Safety notice needed here.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn destroy_map(map_ptr: *mut HashMap<i32, i32>) {
    if map_ptr.is_null() {
        return;
    }
    unsafe {
        drop(Box::from_raw(map_ptr));
    }
}

This is the interface (interface.f90):

module rust_interface

    use, intrinsic :: iso_c_binding, only: c_ptr, c_int32_t

    implicit none

    interface
        function create_map() bind(c)
            import :: c_ptr
            type(c_ptr) :: create_map
        end function

        subroutine insert_into_map(context, key, val) bind(c)
            import :: c_ptr, c_int32_t
            type(c_ptr), value :: context
            integer(c_int32_t), intent(in), value :: key, val
        end subroutine

        function get_from_map(context, key) result(val) bind(c)
            import :: c_ptr, c_int32_t
            type(c_ptr), value :: context
            integer(c_int32_t), intent(in), value :: key
            integer(c_int32_t) :: val
        end function

        subroutine destroy_map(context) bind(c)
            import :: c_ptr
            type(c_ptr), value :: context
        end subroutine
    end interface

end module

And here the example Fortran code (example.f90):

program example

    use, intrinsic :: iso_c_binding, only: c_ptr
    use rust_interface

    implicit none

    type(c_ptr) :: map1, map2

    map1 = create_map()
    map2 = create_map()

    call insert_into_map(map1, 10, 100)
    call insert_into_map(map1, 20, 200)

    call insert_into_map(map2, 20, 300)
    call insert_into_map(map2, 30, 400)

    print *, get_from_map(map1, 10)
    print *, get_from_map(map1, 20)
    print *, get_from_map(map2, 20)

    ! retrieve a non-existent key
    print *, get_from_map(map2, 99)

    print *, get_from_map(map1, 20)
    call insert_into_map(map1, 20, 222)
    print *, get_from_map(map1, 20)

    call destroy_map(map1)
    call destroy_map(map2)

end program

Making the HashMap interface more ergonomic

Having the c_ptr exposed on the Fortran side looks a bit awkward. Also notice that we are passing the context as an argument to every function.

In other languages the % symbol would often be a dot.

We can try to make this more ergonomic by hiding the c_ptr and make the hash table behave more like we interact with such data structures in Rust:

program example

    use rust_interface

    implicit none

    type(map) :: map1, map2

    call map1%init()
    call map2%init()

    call map1%insert(10, 100)
    call map1%insert(20, 200)

    call map2%insert(20, 300)
    call map2%insert(30, 400)

    print *, map1%get(10)
    print *, map1%get(20)
    print *, map2%get(20)

    ! retrieve a non-existent key
    print *, map2%get(99)

    print *, map1%get(20)
    call map1%insert(20, 222)
    print *, map1%get(20)

    call map1%destroy()
    call map2%destroy()

end program

To implement this we don't need to change anything on the Rust side. The necessary changes are highlighted in the interface file (interface.f90):

module rust_interface

    use, intrinsic :: iso_c_binding, only: c_ptr, c_int32_t, c_null_ptr

    implicit none

    type :: map
        type(c_ptr) :: handle
    contains
        procedure :: init    => wrap_create_map
        procedure :: insert  => wrap_insert_into_map
        procedure :: get     => wrap_get_from_map
        procedure :: destroy => wrap_destroy_map
    end type

    interface
        function create_map() bind(c)
            import :: c_ptr
            type(c_ptr) :: create_map
        end function

        subroutine insert_into_map(context, key, val) bind(c)
            import :: c_ptr, c_int32_t
            type(c_ptr), value :: context
            integer(c_int32_t), intent(in), value :: key, val
        end subroutine

        function get_from_map(context, key) result(val) bind(c)
            import :: c_ptr, c_int32_t
            type(c_ptr), value :: context
            integer(c_int32_t), intent(in), value :: key
            integer(c_int32_t) :: val
        end function

        subroutine destroy_map(context) bind(c)
            import :: c_ptr
            type(c_ptr), value :: context
        end subroutine
    end interface

contains

    subroutine wrap_create_map(this)
        class(map), intent(out) :: this

        this%handle = create_map()
    end subroutine


    subroutine wrap_insert_into_map(this, key, val)
        class(map), intent(inout) :: this
        integer(c_int32_t), intent(in), value :: key, val

        call insert_into_map(this%handle, key, val)
    end subroutine


    function wrap_get_from_map(this, key) result(val)
        class(map), intent(in) :: this
        integer(c_int32_t), intent(in), value :: key
        integer(c_int32_t) :: val

        ! no error handling if key is not found
        val = get_from_map(this%handle, key)
    end function


    subroutine wrap_destroy_map(this)
        class(map), intent(inout) :: this

        call destroy_map(this%handle)
        this%handle = c_null_ptr
    end subroutine

end module

Nice! And of course many more improvements are possible but now we have the tools. Now go and have fun using Rust from Fortran! The world is your oyster.

Checking for memory leaks

I used Valgrind to check whether any of the solutions leak memory:

$ valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./example

Maybe you prefer the Rust library in the build dir?

Instead of this (writes to rust/target/):

$ cd rust
$ cargo build --release

You can also do this (writes to build/target/):

$ cd build
$ cargo build --release \
              --manifest-path ../src/rust/Cargo.toml \
              --target-dir $(pwd)/target

Versions used to test the code examples

  • Rust 1.85.0
  • CMake 3.30.5
  • GNU Fortran (GCC) 13.3.0