Rust FFI Programming - Binding a C Library in Rust
Although crates.io provides over 164,780 packages (as of December 2024), there are still situations where we might encounter the absence of ready-made packages in the Rust ecosystem. To address this issue, we can bind C libraries and use FFI (Foreign Function Interface) to call these libraries. In fact, many popular Rust packages also rely on FFI to call C libraries, such as openssl and git, which are not purely implemented in Rust.
Next, we will introduce how to bind a C library and use it in Rust.
In Rust, all calls to external libraries are considered unsafe operations and therefore require the unsafe keyword to declare an unsafe code block. The extern block is used to declare function signatures from external libraries. A typical extern block looks like this:
unsafe extern "C" {
///...
}
Here, C indicates that these functions or variables follow the calling convention of the C language.
Although we can manually implement the binding of a C library, using the bindgen tool can save a lot of effort.
Here, we will use the pdfio library as an example.
Installing pdfio
pdfio needs to be compiled from source, so first download the source package.
wget https://github.com/michaelrsweet/pdfio/releases/download/v1.3.2/pdfio-1.3.2.tar.gz
tar -zxvf pdfio-1.3.2.tar.gz
cd pdfio-1.3.2
On Unix systems, PDFio uses a configuration script to generate a makefile.
./configure
# Or use a dynamic library
./configure --enable-shared
The default installation location is /usr/local. Use the --prefix option to install it in another location.
After configuration, run the following command to build the library:
make all
Run the installation command:
sudo make install
pdfio depends on ZLIB (https://www.zlib.net) 1.1 by default, so we also need to install zlib.
Here is an example for macOS:
brew install zlib
export LDFLAGS="-L/opt/homebrew/opt/zlib/lib"
export CPPFLAGS="-I/opt/homebrew/opt/zlib/include"
At this point, pdfio has been installed.
Preparing the Project
Let’s create a project called pdfio-rs.
cargo new pdfio-rs --lib
Open the Cargo.toml file and add dependencies:
[build-dependencies]
bindgen = "0.71.0"
We need bindgen to generate the binding code.
Creating the wrapper.h Header
We need a header file to include all the headers, so we create a wrapper.h file with the following content:
#include </usr/local/include/pdfio.h>
If there are multiple header files, you can list them all in this file.
Build Script
We also need to create a build.rs file to generate the binding code during the build process. The content is as follows:
// build.rs
use std::env;
use std::path::PathBuf;
fn main() {
// Tell cargo to look for static libraries in the specified directory
println!("cargo:rustc-link-lib=z");
println!("cargo:rustc-link-search=native=/opt/homebrew/opt/zlib/lib");
// Tell cargo to look for shared libraries in the specified directory
println!("cargo:rustc-link-search=/usr/local/lib");
// Tell rustc to link the system shared library.
// The library name is libpdfio.so or libpdfio.a
println!("cargo:rustc-link-lib=pdfio");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}
Here, we need to handle the linking of the zlib and pdfio libraries. Use println! to tell rustc the location of the system shared libraries.
Note that in cargo:rustc-link-lib={}, the library name does not include the prefix, e.g., for libpdfio.so, you only need to write pdfio.
The reason for cargo:rustc-link-lib=z is that the name of zlib on macOS is libz.dylib.
Including the Generated Binding Code in the Library
We can use the include! macro to import the generated bindings directly into the main entry point of our crate, src/lib.rs.
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
Since the naming conventions in the code generated by bindgen do not conform to Rust’s naming conventions, we need to use macros like #![allow(non_upper_case_globals)] to ignore warnings.
In fact, the code here is already written and can be used directly.
However, if that’s all, there would be a lot of unsafe code when calling externally. Additionally, the parameters used in these functions come from equivalent C parameters and are not typical Rust types, making it difficult for callers.
As the library author, we should try to avoid having the caller use unsafe code and make the library more Rust-like. Therefore, we need to add some encapsulation code in src/lib.rs.
Encapsulation
To increase usability, we have added the following encapsulation code, defining a struct:
struct Pdfio {
file: *mut pdfio_file_t,
}
Due to space limitations, we only list the opening a PDF and getting the number of pages in a PDF functionalities.
Opening a PDF
Let’s look at the original C function signature:
pdfio_file_t *pdfioFileOpen(const char *filename, pdfio_password_cb_t password_cb, void *password_cbdata, pdfio_error_cb_t error_cb, void *error_cbdata);
Parameter meanings:
filename: File namepassword_cb: Password callback, NULL means nonepassword_cbdata: Password callback dataerror_cb: Error callback, NULL means noneerror_cbdata: Error callback data
The bound signature is as follows:
unsafe extern "C" {
pub fn pdfioFileOpen(
filename: *const ::std::os::raw::c_char,
password_cb: pdfio_password_cb_t,
password_data: *mut ::std::os::raw::c_void,
error_cb: pdfio_error_cb_t,
error_data: *mut ::std::os::raw::c_void,
) -> *mut pdfio_file_t;
}
It is clear that without encapsulation, it would be difficult for the caller to use.
To simplify, we will not handle password and error issues, only the simplest file opening.
pub fn file_open(filename: &str) -> Self {
unsafe {
let filename_cstr = std::ffi::CString::new(filename).unwrap();
let filename_ptr = filename_cstr.as_ptr();
let password_cb = None;
let password_data: *mut ::std::os::raw::c_void = std::ptr::null_mut();
let error_cb = None;
let error_data: *mut ::std::os::raw::c_void = std::ptr::null_mut();
let f = pdfioFileOpen(filename_ptr, password_cb, password_data, error_cb, error_data);
Self { file: f }
}
}
Getting the Number of Pages in a PDF
This is relatively simple, just call the pdfioFileGetNumPages function to return the number of pages.
pub fn get_page_count(&self) -> usize {
unsafe {
pdfioFileGetNumPages(self.file) as usize
}
}
Destructor
When the PdfFile struct is dropped, the internal file pointer needs to be released. Remember the principle: who creates the pointer, who is responsible for releasing it.
We need to implement Rust’s destructor to provide safety and ensure the release of these resources.
impl Drop for Pdfio {
fn drop(&mut self) {
unsafe {
pdfioFileClose(self.file);
}
}
}
Finally, add tests:
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_page_count() {
let pdf = Pdfio::file_open("test.pdf");
assert_eq!(pdf.get_page_count(), 1);
}
}
Run the tests:
cargo test --package pdfio-rs --lib -- test --show-output