Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Link Fortran with Rust: Is It Really That Hard?

Learn how to link Fortran with Rust using build.rs, cargo scripts and best practices for scientific computing.
Visual concept of linking Fortran and Rust programming languages, with split-screen code styles, connected by electric arc and explosive graphic in center symbolizing interoperability Visual concept of linking Fortran and Rust programming languages, with split-screen code styles, connected by electric arc and explosive graphic in center symbolizing interoperability
  • 🧠 Rust's ownership model and FFI support can safely use legacy Fortran code for high-performance scientific computing.
  • 📊 Fortran’s numerical performance is still faster than many modern alternatives, especially with optimized libraries like BLAS or LAPACK.
  • 🔗 bind(C) in Fortran provides ABI compatibility and avoids linker errors when called from Rust.
  • ⚠️ Most cross-platform FFI issues come from name mangling and compiler differences, not code-level bugs.
  • 🔒 Breaking FFI into modules with safe Rust wrappers keeps unsafe logic separate and makes it simpler to maintain.

Rust and Fortran might seem like an odd pair—one is a 21st-century leader in safe systems programming, and the other is a decades-old, well-known tool in scientific computation. But when fast, accurate work meets safe, modern code, you can do a lot. This article shows how to link Fortran to Rust using Rust FFI. It provides a full guide to techniques, problems, and good practices for developers who want to use both languages for fast, reliable, and maintainable software.


Understanding Rust FFI

Rust’s Foreign Function Interface (FFI) is the main way languages work together in multi-language systems. It lets Rust code call—and be called by—functions that follow the C Application Binary Interface (ABI). While made for C compatibility, this also lets you link Fortran—with the right preparations.

Key Rust FFI Concepts

  • extern "C" Declarations
    These tell the Rust compiler the function you’re linking to uses the C calling convention. The "C" keyword makes sure of ABI compatibility and allows linkage during compilation.

    MEDevel.com: Open-source for Healthcare and Education

    Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

    Visit Medevel

  • unsafe Blocks
    All FFI calls in Rust are marked unsafe because the compiler cannot check memory safety promises. This is a promise: you're responsible for making sure no things go wrong.

  • FFI Use Cases
    Use Rust FFI when:

    • You have a Fortran module that works well and has known performance.
    • You’re slowly moving your code to Rust.
    • You depend on numerical libraries with no similar tools in Rust (e.g., LAPACK).

Basic Example

extern "C" {
    fn add_numbers(a: f64, b: f64) -> f64;
}

fn main() {
    unsafe {
        println!("Sum: {}", add_numbers(2.0, 3.5));
    }
}

This example assumes the add_numbers function exists in an external shared/static library, compiled with a Fortran or C compiler that follows the appropriate ABI.


Fortran’s ABI and Binding Expectations

Fortran, depending on compiler and version, changes function names in different ways. This often stops languages from linking. Understanding and controlling this behavior is key when working with Rust FFI.

Common Fortran Name Mangling Patterns

  • Function name lowercasing: ADD_NUMBERS becomes add_numbers.
  • Trailing underscores: gfortran may convert add_numbers into add_numbers_.
  • Module scoping: Functions inside modules might become __module_MOD_func or worse.

The Modern Solution: bind(C)

The bind(C) attribute in newer Fortran compilers (Fortran 2003+) lets you stop name changes and make sure it works with C ABI.

subroutine add_numbers(a, b, result) bind(C, name="add_numbers")
  use iso_c_binding
  real(c_double), intent(in)  :: a, b
  real(c_double), intent(out) :: result
end subroutine add_numbers

This makes sure that the subroutine is exported directly as add_numbers, with C-compatible types and how it is called.


Preparing Fortran Code for Rust FFI

To connect them, you need to make sure the Fortran code works in a way Rust understands.

Best Practices

  1. Use iso_c_binding: Makes sure all types match up with C (and thus Rust) expectations. For example:
    • c_double maps to Rust's f64
    • c_int maps to Rust's i32
  2. Avoid COMMMON blocks: Shared memory segments can make the memory hard to see and unsafe to handle from Rust.
  3. Prefer subroutines over functions: Rust works better with external C functions when Fortran uses subroutine, especially when returning complex results.
  4. Use fixed-size arrays carefully: Fortran uses column-major order, but Rust uses row-major.

Sample Fortran Subroutine

subroutine matrix_multiply(a, b, c, n) bind(C, name="matrix_multiply")
  use iso_c_binding
  integer(c_int), intent(in) :: n
  real(c_double), intent(in)  :: a(n,n), b(n,n)
  real(c_double), intent(out) :: c(n,n)
end subroutine

This is good for linking with Rust. All parameters are passed by reference, and types are predictable.


Declaring Fortran Functions in Rust

To call Fortran code from Rust, you must declare an extern "C" function with types and pointers matching the Fortran signature.

Rust FFI Declaration for matrix_multiply

extern "C" {
    fn matrix_multiply(
        a: *const f64,
        b: *const f64,
        c: *mut f64,
        n: i32,
    );
}

Example Usage

let n = 3;
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; // Column-major layout
let b = a.clone();
let mut c = vec![0.0f64; n * n];

unsafe {
    matrix_multiply(a.as_ptr(), b.as_ptr(), c.as_mut_ptr(), n as i32);
}

Tip: You may need to convert to/from column-major layout. Rust’s ndarray crate can help here.


Automating Fortran Builds with build.rs

Rust’s build script system lets you set up special compilation steps. The cc crate lets you call Fortran compilers easily as part of the build process.

Sample build.rs

fn main() {
    cc::Build::new()
        .file("src/compute.f90")
        .compiler("gfortran")
        .flag("-O3")
        .compile("libcompute.a");
}

This tells Cargo to compile src/compute.f90 into a static library libcompute.a using gfortran at optimization level 3.

Add to Cargo.toml

[build-dependencies]
cc = "1.0"

File Structure

project-root/
├── src/
│   ├── main.rs
│   └── compute.f90
├── build.rs
├── Cargo.toml

Cross-Platform Compatibility

Running your project across macOS, Linux, and Windows shows problems in FFI decisions. Many compilers, linkers, and ABIs behave differently.

Tips for Multi-Platform FFI

  • On macOS, link with System:

    println!("cargo:rustc-link-lib=dylib=System");
    
  • On Linux:

    println!("cargo:rustc-link-lib=gfortran");
    println!("cargo:rustc-link-lib=m");
    
  • Use std::env::var("TARGET") to detect the platform and build differently for each one.

Cross Compilation

If you're building for ARM systems or unusual environments, set up cross-compilation toolchains for both Rust and Fortran. Consider cross or cargo-zigbuild to make this easier.


Troubleshooting: Top Issues in Fortran Rust Linking

When linking Fortran to Rust, most issues aren’t deep bugs—they’re things that don't match up. Below are common errors and how to find out what's wrong.

1. Symbol Not Found

  • Issue: Rust expects add_numbers, but the Fortran compiler exported add_numbers_.
  • Fix: Use bind(C, name="add_numbers") in Fortran to set the symbol exactly.

2. Segmentation Faults

  • Cause: Misaligned or uninitialized pointers. Fortran expects inputs by reference.
  • Fix:
    • Check lifetime of Rust buffers.
    • Make sure matrix dimensions and declared sizes match exactly.

3. Undefined Reference Errors

  • Cause: Link order or omitted dependencies.
  • Fix: If linking manually, fortran libraries come after Rust binaries. Make sure cargo:rustc-link-lib includes everything.

Debugging Tools

  • nm or objdump -t shows symbols exported by .o or .a files.
  • Use println! in Rust and write(*,*) in Fortran for temporary output.
  • Debug with gdb or lldb, setting breakpoints in both Rust and Fortran code.

Advanced Integration: Rust with LAPACK

The lapack and blas-src crates let you use fast linear algebra libraries, most written in Fortran.

Example: Calling dgesv_ for Solving Linear Systems

extern "C" {
    fn dgesv_(
        n: *const i32,
        nrhs: *const i32,
        a: *mut f64,
        lda: *const i32,
        ipiv: *mut i32,
        b: *mut f64,
        ldb: *const i32,
        info: *mut i32,
    );
}

You must link against system LAPACK (OpenBLAS, MKL, etc.):

println!("cargo:rustc-link-lib=lapack");
println!("cargo:rustc-link-lib=blas");

Performance Tip

When solving large matrix problems (n > 1000), calling LAPACK from Rust remains faster than most pure-Rust alternatives.


Testing and Debugging Mixed Codebases

Testing Rust code that depends on Fortran-linked functions can be hard.

Solutions

  • Mock Fortran logic in tests:

    #[cfg(test)]
    fn add_numbers(a: f64, b: f64) -> f64 { a + b }
    
  • Use #[cfg(test)] to compile either mock or real functions.

  • Advanced debugging with gdb or lldb: Stepping through both Fortran and Rust source files helps find tricky bugs.


Best Practices for Maintainability

Modular Design for FFI

Create a bindings or ffi module to separate all unsafe blocks and keep your application logic clean.

pub mod ffi {
    extern "C" {
        pub fn add_numbers(a: f64, b: f64) -> f64;
    }

    pub fn safe_add(a: f64, b: f64) -> f64 {
        unsafe { add_numbers(a, b) }
    }
}

Document Assumptions

  • Clearly state ABI conventions.
  • Note required compiler versions.
  • Track Fortran changes that can affect Rust logic.

When (and If) to Migrate Fortran to Rust

Sometimes, it’s better to move all your code to Rust. But when should you?

Signs It’s Time to Migrate

  • Your development team struggles with old tooling.
  • Memory bugs slip through despite a lot of testing.
  • Your use case needs a lot of low-level work like networking or parallelism.

When to Stick With Linking

  • Your Fortran code works very well for numerical routines.
  • You rely on tested Fortran libraries like FFTPACK or LAPACK.
  • Speed is critical, and rewriting it might make it worse.

C Bridge Strategy

Make a “middle layer” in C that connects Fortran and Rust, especially for complex APIs. This can reduce problems between Rust and Fortran.


Final Thoughts: Use Both Languages Well

Connecting Fortran and Rust isn't just possible—it's useful and often very fast. By understanding FFI, managing ABI compatibility with bind(C), and wrapping unsafe code carefully, you can use Fortran's many math tools and get the good parts of Rust’s safety and tooling.

Use Rust where safety and clarity matter. Use Fortran where mature math matters. And link them like a pro.

As they say, make it work first. Then, make it right. Then, make it fast—and you’ll find Fortran Rust linking gives you all three.


Citations

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading