Fortran calling Rust
A tour of interfacing Fortran with Rust through a C interface
March 23, 2025
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?
- How?
- Functions which return a single value
- Do we need to worry about the C naming convention?
- Should we not compile the Rust library as part of CMake?
- Working with arrays
- Maintaining a context on the Rust side
- Example: Using Rust's HashMap in Fortran
- Making the HashMap interface more ergonomic
- Checking for memory leaks
- Maybe you prefer the Rust library in the build dir?
- Versions used to test the code examples
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