Calling C from Rust

Today, let’s look at how to call C functions from Rust.

Suppose we have a library written in C, say

// /project/root/mathlib.h
double add(double x, double y);

// /project/root/mathlib.c
#include <math.h>

double add(double x, double y) {
    return x + y;
}

To call the function add() from Rust, we import the external function with extern keyword

// /project/root/src/main.rs
unsafe extern "C" {
    fn add(x: f64, y: f64) -> f64;
}

fn main() {
    unsafe {
        let result = add(2.0, 3.0);
        println!("The result of addition is: {}", result);
    }
}

Static Linking

First, let’s link it statically. That is, our Rust executable will embed the C library code in itself.

# we assume we are at /project/root/ directory

# create a static object file
gcc -fPIC -c -o libmathlib.o mathlib.c

# create an archive
ar rucs libmathlib.a libmathlib.o

Next, we need to create build.rs file that will link the C object file into the Rust executable

// /project/root/build.rs
use std::env;

fn main() {
    let pwd = env::var("PWD").unwrap();

    println!("cargo::rustc-link-search={}", pwd);
    println!("cargo::rustc-link-lib=mathlib");
    println!("cargo::rerun-if-changed=mathlib.c");
}

Now, we are ready to build and run!

# build
cargo build

# run
target/debug/calling-c-from-rust
The result of addition is: 5

We can verify that the libmathlib.a is statically linked

# libmathlib.a is not found in the dynamically linked dependencies
ldd target/debug/calling-c-from-rust
 linux-vdso.so.1 (0x0000ffffb434c000)
 libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffffb4250000)
 libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffffb4090000)
 /lib/ld-linux-aarch64.so.1 (0x0000ffffb4310000)

Dynamic Linking

Next up, let’s see what changes if we want to dynamically link. The only change is that we need to compile into a shared library rather than static one.

gcc -shared -fPIC mathlib.c -o libmathlib.so

Note that we don’t need to invoke ar this time. The rest is exactly the same. Let’s build it again

# build
cargo build

# run
target/debug/calling-c-from-rust
The result of addition is: 5

Now, let’s make sure it is dynamically linked

# we see libmathlib.so
ldd target/debug/calling-c-from-rust
 linux-vdso.so.1 (0x0000ffff93eec000)
 libmathlib.so => /root/rust-and-c/libmathlib.so (0x0000ffff93e00000)
 libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff93dc0000)
 libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff93c00000)
 /lib/ld-linux-aarch64.so.1 (0x0000ffff93eb0000)

As expected, we do see libmathlib.so file appear.

Below is the full script that reproduces the steps here.

# clean up from previous run if any
mkdir -p calling-c-from-rust
rm -rf calling-c-from-rust

# create a new rust project from scratch
cargo new calling-c-from-rust
cd calling-c-from-rust

# write C header and source files
cat << EOF > mathlib.h
double add(double x, double y);
EOF

cat << EOF > mathlib.c
#include <math.h>

double add(double x, double y) {
    return x + y;
}
EOF

# write rust program that calls add() function
cat << EOF > src/main.rs
unsafe extern "C" {
    fn add(x: f64, y: f64) -> f64;
}

fn main() {
    unsafe {
        let result = add(2.0, 3.0);
        println!("The result of addition is: {}", result);
    }
}
EOF

# static linking
gcc -fPIC -c -o libmathlib.o mathlib.c
# linux
ar rucs libmathlib.a libmathlib.o
# macos (uncomment below)
# libtool -static -o libmathlib.a mathlib.o

# dynamic linking (uncomment below)
# gcc -fPIC -shared -o libmathlib.so mathlib.c

cat << EOF > build.rs
use std::env;

fn main() {
    let pwd = env::var("PWD").unwrap();

    println!("cargo::rustc-link-search={}", pwd);
    println!("cargo::rustc-link-lib=mathlib");
    println!("cargo::rerun-if-changed=mathlib.c");
}
EOF

cargo run

References