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?