Search code examples
rustbuildrust-cargoffi

Trouble with undefined symbols in Rust's ffi when using a C/Fortran library


I have a library, namely pfapack (1), that I want to use in rust. So the initial code is written in Fortran, but a C interface exists and works well. I want to make a Rust crate (2) that ships this code so I can use it in any other Rust project (3). Doing so, (3) gives an undefined symbol error.

I have written a build script in (2) that calls (1)'s build method. I then use cc to combine the object files and link the needed libraries. I then used bindgen to generate bindings for the functions I need. I would expect that (3) would be able to see the object that were compiled at (2) build time, but it can't. The exact step taken are:

  • New crate with (1)'s source code.
  • (2) build.rs
use std::process::Command;

fn main() {
// This makefile call a custom root makefile that only calls the two
// makefiles in c_interface/ and fortran/
    Command::new("make").output() 
        .expect("Failed to make");

    println!("cargo:rustc-link-search=c_interface");
    println!("cargo:rustc-link-search=fortran");
    println!("cargo:rustc-link-lib=static=pfapack");
    println!("cargo:rustc-link-lib=static=cpfapack");
    println!("cargo:rustc-link-lib=gfortran");

}
  • (3) build.rs
fn main() {
    println!("cargo:rustc-link-lib=lapack");
    println!("cargo:rustc-link-lib=blas");
}

Original example compilation to use pfapack

gcc -O3 -I c_interface/ foo.c -o foo.out c_interface/libcpfapack.a fortran/libpfapack.a -lm -lblas -llapack -lgfortran

The command used to generate bindings came from https://github.com/blas-lapack-rs/lapack-sys/blob/master/bin/generate.sh as it uses the same naming convention:
generate.sh

#!/bin/bash
set -eux

bindgen --allowlist-function='^.*_$' --use-core pfapack.h \
  | sed -e 's/::std::os::raw:://g' \
  | sed -e '/__darwin_size_t/d' \
  > pfapack.rs

rustfmt pfapack.rs

Compiling (3) gives this error https://pastebin.com/4FubsYx9 Ignoring a big blob of flags, the error:

  = note: /usr/bin/ld: /home/dumbo/Documents/test_pfapack/target/debug/deps/test_pfapack-d08bc25fe63b6ef8.ka29pyd46xgunxk.rcgu.o: in function `pfapack_sys::dskpfa':
          /home/dumbo/Documents/pfapack-sys/src/pfapack-bind.rs:249: undefined reference to `dskpfa'
          collect2: error: ld returned 1 exit status
 
  = note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified
  = note: use the `-l` flag to specify native libraries to link
  = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-link-libkindname)
 
error: could not compile `test_pfapack` due to previous error

(2) Cargo.toml

[package]
name = "pfapack-sys"
version = "0.1.0"
edition = "2021"
links = "pfapack"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
libc = "0.2"

[dependencies.num-complex]
version = "0.4"
default-features = false

[lib]
name = "pfapack_sys"

[build-dependencies]
cc = "1.0.79"

Solution

  • The solution was quite simple and right under my nose. Note that the symbol not found does not have an ending underscore, as pfapack functions do after mangling. This was because I tried to do the bindings to the C interface and the pretty Rust functions in the same crate. Looking at the Rust book, https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages, the convention is to have a *-sys crate that just does the binding, and have another crate named * that does the pretty functions. Thus, I needed to update the build script. Here is the updated (2) build.rs

    // build script
    use std::process::Command;
    
    fn main() {
        Command::new("make").output()
            .expect("Failed to make");
    
        println!("cargo:rustc-link-search=c_interface");
        println!("cargo:rustc-link-search=fortran");
        println!("cargo:rustc-link-lib=static=pfapack");
        println!("cargo:rustc-link-lib=static=cpfapack");
        println!("cargo:rustc-link-lib=gfortran");
        println!("cargo:rustc-link-lib=lapack");
        println!("cargo:rustc-link-lib=blas");
    }
    

    I renamed this package pfapack-sys. Created a new crate named pfapack(4) that depends on pfapack-sys. Now, update Cargo.toml(3) to depend on (4). Now works out of the box.