Often times C and C++ headers will have platform- and architecture-specific
#ifdefs that affect the shape of the Rust FFI bindings we need to create to
interface Rust code with the outside world. The state of the art solution so far
has been to maintain a different set of bindings for each of our supported
platforms. This might be a manual process if we’re writing our FFI bindings by
hand, or slightly (and only slightly) less manual if we’re running bindgen
once on each supported platform and checking in the generated bindings.
The result has been that maintaining Rust FFI bindings to C and C++ libraries
has been tedious, even with bindgen to help automate some bits.
Recently, we exposed library usage of bindgen that enables us to put bindgen
in the [build-dependencies] section of a crate’s Cargo.toml file and
generate bindings for the current platform on-the-fly from inside a build.rs
file.
No more need to manually generate and check-in into our repository a different
set of bindings for each supported platform!
What follows is a whirlwind introductory tutorial to this brave new
bindgen + build.rs world. We’ll generate bindings to bzip2 (which is
available on most systems) on-the-fly.
Note: we won’t be publishing these bindings on crates.io becuase there is
already a bzip2-sys raw FFI crate and a bzip2 crate
providing a nice Rust-y API built on top of that. This tutorial is only for
exposition!
Step 1: Adding bindgen as a Build Dependency
Declare a build-time dependency on bindgen by adding it to the
[build-dependencies] section of our crate’s Cargo.toml metadata file:
Step 2: Create a wrapper.h Header
The wrapper.h file will include all the various headers containing
declarations of structs and functions we would like bindings for. In the
particular case of bzip2, this is pretty easy since the entire public API is
contained in a single header. For a project like SpiderMonkey,
where the public API is split across multiple header files and grouped by
functionality, we’d want to include all those headers we want to bind to in this
single wrapper.h entry point for bindgen.
Here is our wrapper.h:
Step 3: Create a build.rs File
First, we have to tell cargo that we have a build.rs script by adding
another line to the Cargo.toml:
Second, we create the build.rs file in our crate’s root. This file is compiled
and executed before the rest of the crate is built, and can be used to generate
code at compile time. And of course in our case, we will be generating Rust FFI
bindings to bzip2 at compile time. The resulting bindings will be written to
$OUT_DIR/bindings.rs where $OUT_DIR is chosen by cargo and is something
like ./target/debug/build/libbindgen-tutorial-bzip2-sys-afc7747d7eafd720/out/.
Now, when we run cargo build, our bindings to bzip2 are generated on the
fly!
Step 4: Include the Generated Bindings in src/lib.rs
We can use the include! macro to dump our generated bindings right into our
crate’s main entry point, src/lib.rs:
Because bzip2’s symbols do not follow Rust’s style conventions, we suppress a
bunch of warnings with a few #![allow(...)] pragmas.
We can run cargo build again to check that the bindings themselves compile:
And we can run cargo test to verify that the layout, size, and alignment of
our generated Rust FFI structs match what bindgen thinks they should be:
Step 5: Write a Sanity Test
Finally, to tie everything together, let’s write a sanity test that round trips
some text through compression and decompression, and then asserts that it came
back out the same as it went in. This is a little wordy using the raw FFI
bindings, but hopefully we wouldn’t usually ask people to do this, we’d provide
a nice Rust-y API on top of the raw FFI bindings for them. However, since this
is for testing the bindings directly, our sanity test will use the bindings
directly.
The test data I’m round tripping are some Futurama quotes I got off the internet
and put in the futurama-quotes.txt file, which is read into a &'static str
at compile time via the include_str!("../futurama-quotes.txt") macro
invocation.
Without further ado, here is the test, which should be appended to the bottom of
our src/lib.rs file:
Now let’s run cargo test again and verify that everying is linking and binding
properly!
Step 6: Publish Your Crate!
That’s it! Now we can publish our crate on crates.io and we can write a nice,
Rust-y API wrapping the raw FFI bindings in a safe interface. However, there is
already a bzip2-sys crate providing raw FFI bindings, and there is
already a bzip2 crate providing a nice, safe, Rust-y API on top of the
bindings, so we have nothing left to do here!