Finding Libraries with pkg-config

Many C libraries install their headers somewhere that clang does not search by default. Some keep them in a versioned subdirectory such as /usr/include/libpng16, and package managers like Homebrew install them under their own prefix. In that case bindgen fails with an error like fatal error: 'png.h' file not found.

Such libraries usually ship a pkg-config file describing where their headers and binaries live. The pkg-config crate can query it from build.rs, and we can pass the include paths it finds to bindgen.

First, add pkg-config to the [build-dependencies] section next to bindgen:

[build-dependencies]
bindgen = "0.73.2"
pkg-config = "0.3"

Our wrapper.h includes the library's header as usual:

#include <png.h>

Then, in build.rs, we probe for the library and turn each include path into a -I argument for clang:

use std::env;
use std::path::PathBuf;

fn main() {
    // Ask pkg-config for `libpng`. On success, this also prints the
    // `cargo:rustc-link-search` and `cargo:rustc-link-lib` lines needed to
    // link against the library.
    let library = pkg_config::probe_library("libpng")
        .expect("Unable to find libpng with pkg-config");

    let bindings = bindgen::Builder::default()
        // The input header we would like to generate
        // bindings for.
        .header("wrapper.h")
        // Tell clang where to find the headers included by `wrapper.h`.
        .clang_args(
            library
                .include_paths
                .iter()
                .map(|path| format!("-I{}", path.display())),
        )
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");

    // Write the bindings to the $OUT_DIR/bindings.rs file.
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

A few things to note:

  • bindgen only generates the Rust declarations, it does not link anything. Linking is handled by pkg_config::probe_library, which prints the needed cargo:rustc-link-* lines, so there is no need to write them by hand as in the tutorial.
  • bindgen::CargoCallbacks prints a cargo:rerun-if-changed line for wrapper.h and for every header it includes, so the bindings are regenerated when the library's headers change. The pkg-config crate prints cargo:rerun-if-env-changed lines for the environment variables it reads, such as PKG_CONFIG_PATH.
  • Some libraries also require preprocessor definitions. These are available in library.defines and can be passed to clang as -D arguments in the same way.
  • Use pkg_config::Config instead of probe_library to require a minimum version, prefer static linking, and so on.