๐ŸŽ“ A1Learner
๐Ÿ”

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?