🎓 A1Learner
🔍
0 of 149 lessons completed

Rust FFI Tooling: Link and Bindgen

FFI tooling is the set of helpers that link a C library into a Rust project. bindgen writes the extern declarations from a C header.

Name the library on the block

The link attribute names the library behind the block. This one calls the real sqrt from libm:

#[link(name = "m")]
unsafe extern "C" {
    fn sqrt(input: f64) -> f64;
}

Tell Cargo where to look

Two printed lines are the whole conversation. Cargo reads them as instructions, not as output.

fn main() {
    println!("cargo::rustc-link-search=/path/to/lib");
    println!("cargo::rustc-link-lib=counter");
}

Let cc compile the C side

This build script compiles counter.c into a static library. Cargo links it with no more instructions.

fn main() {
    cc::Build::new()
        .file("counter.c")
        .compile("counter");
}

A header goes in

bindgen reads the header. It needs only the promises, never the C source.

// counter.h
typedef struct Counter Counter;
Counter* counter_new(int start);
int counter_get(const Counter* c);

Declarations come out

The output has the same shape you wrote by hand in the opaque types lesson. The machine just types faster.

// the shape bindgen writes for that header
pub struct Counter {
    _private: [u8; 0],
}
unsafe extern "C" {
    pub fn counter_new(start: i32) -> *mut Counter;
}

Example

// build.rs
fn main() {
    cc::Build::new().file("counter.c").compile("counter");
}

// src/main.rs
use std::ffi::c_int;

#[repr(C)]
pub struct Counter {
    _private: [u8; 0],
}

unsafe extern "C" {
    fn counter_new(start: c_int) -> *mut Counter;
    fn counter_add(c: *mut Counter, amount: c_int);
    fn counter_get(c: *const Counter) -> c_int;
    fn counter_free(c: *mut Counter);
}

fn main() {
    let counter = unsafe { counter_new(10) };
    unsafe {
        counter_add(counter, 5);
        counter_add(counter, 3);
        println!("total = {}", counter_get(counter));
        counter_free(counter);
    }
}

Key points

  • Put #[link(name = "counter")] on an extern block to name the library it needs.
  • kind = "static" or kind = "dylib" says which kind to pick
  • A build script is a Rust file named build.rs at the package root.
  • Cargo compiles it and runs it before your crate
  • Use println! lines in build.rs to talk to Cargo.
  • cargo::rustc-link-search names a folder.
  • cargo::rustc-link-lib names a library
  • Use the cc crate in build.rs to compile C files into a static library.
  • It picks the right compiler and flags for the platform
  • Use bindgen to read a C header and write the extern block for you.
  • Big libraries have hundreds of functions.
  • Nobody types them all by hand
  • The tooling arranges the link, so the Rust code only declares the functions.
  • Generated bindings are still unsafe to call.
  • bindgen copies the C contract faithfully.

In this example, the project has two halves. The Cargo.toml lists cc under build-dependencies. When you run cargo run, Cargo first compiles and runs build.rs. The cc crate compiles counter.c into a static library and links it. Only then does Cargo build main.rs. The Rust code declares the same opaque Counter as before. It never names a library file or a folder. The build script arranged all of that. The program prints total = 18 from the real C counter.

Note

It cannot check the contract, and neither can the compiler

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Your build.rs must tell Cargo that a C library lives in the folder vendor/lib. Which line does it?