Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Build Time Tooling

This section covers “build-time” tooling, or code that is run prior to compiling a crate’s source code. Conventionally, build-time code lives in a build.rs file and is commonly referred to as a “build script”. Common use cases include rust code generation and compilation of bundled C/C++/asm code. See crates.io’s documentation on the matter for more information.

C

cc Development tools

To accommodate scenarios where additional C, C++, or assembly is required in a project, the cc crate offers a simple api for compiling bundled C/C++/asm code into static libraries (.a) that can be statically linked to by rustc.

The following recipe has some bundled C code (src/hello.c) that will be used from rust. Before compiling rust source code, the “build” file (build.rs) specified in Cargo.toml runs. Using the cc crate, a static library file will be produced (in this case, libhello.a, see compile docs) which can then be used from rust by declaring the external function signatures in an extern block.

Since the bundled C is very simple, only a single source file needs to be passed to cc::Build. For more complex build requirements, cc::Build offers a full suite of builder methods for specifying include paths and extra compiler flags.

build.rs

fn main() {
    cc::Build::new().file("src/hello.c").compile("hello"); // outputs `libhello.a`
}

src/hello.c

#include <stdio.h>


void hello() {
    printf("Hello from C!\n");
}

void greet(const char* name) {
    printf("Hello, %s!\n", name);
}

src/main.rs

use anyhow::Result;
use std::ffi::CString;
use std::os::raw::c_char;

fn prompt(s: &str) -> Result<String> {
    use std::io::Write;
    print!("{}", s);
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    Ok(input.trim().to_string())
}

unsafe extern "C" {
    fn hello();
    fn greet(name: *const c_char);
}

fn main() -> Result<()> {
    unsafe { hello() }
    let name = prompt("What's your name? ")?;
    let c_name = CString::new(name)?;
    unsafe { greet(c_name.as_ptr()) }
    Ok(())
}

Compile a C library while setting custom defines

cc Development tools

It is simple to build bundled C code with custom defines using cc::Build::define. The method takes an Option value, so it is possible to create defines such as #define APP_NAME "foo" as well as #define WELCOME (pass None as the value for a value-less define). This recipe builds a bundled C file with dynamic defines set in build.rs and prints “Welcome to foo - version 1.0.2” when run. Cargo sets some environment variables which may be useful for some custom defines.

build.rs

fn main() {
    cc::Build::new()
        .define("APP_NAME", "\"foo\"")
        .define(
            "VERSION",
            format!("\"{}\"", env!("CARGO_PKG_VERSION")).as_str(),
        )
        .define("WELCOME", None)
        .file("src/foo.c")
        .compile("foo");
}

src/foo.c

#include <stdio.h>

void print_app_info() {
#ifdef WELCOME
    printf("Welcome to ");
#endif
    printf("%s - version %s\n", APP_NAME, VERSION);
}

src/main.rs

unsafe extern "C" {
    fn print_app_info();
}

fn main() {
    unsafe {
        print_app_info();
    }
}

cc Development tools

Often the C code you need is not yours: it is a third-party library whose source you vendor into your project, with its own layout of public headers (include/) and implementation files (src/). The cc crate can compile such a library from build.rs into a static archive (libfnv.a) that rustc links into the final binary, so the resulting executable has no runtime dependency on the library.

The following recipe vendors a tiny hashing library (a 64-bit FNV-1a implementation) in vendor/fnv. cc::Build::include adds the library’s header directory to the compiler’s search path, and cc::Build::file adds its source. Because third-party code is not ours to fix, cc::Build::warnings is set to false so the library’s warnings do not clutter the build output. cc::Build::try_compile returns a Result instead of panicking and also emits the cargo:rustc-link-lib=static=fnv and cargo:rustc-link-search directives, so no manual linking instructions are needed. The cargo:rerun-if-changed line re-runs the build script when the vendored library changes.

The Rust side declares the function’s signature in an extern block, as in the bundled C library recipe, and wraps the unsafe call in a safe function.

To copy this recipe, copy the whole cc_external_static crate into your project and replace vendor/fnv with your own library.

build.rs

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    // The external library keeps its own layout: public headers in `include/`
    // and implementation files in `src/`.
    let lib_dir = "vendor/fnv";

    cc::Build::new()
        .include(format!("{lib_dir}/include"))
        .file(format!("{lib_dir}/src/fnv.c"))
        // Third-party code is not ours to fix, so don't fail on its warnings.
        .warnings(false)
        .try_compile("fnv")?; // outputs `libfnv.a` and emits `cargo:rustc-link-lib=static=fnv`

    println!("cargo:rerun-if-changed={lib_dir}");
    Ok(())
}

vendor/fnv/include/fnv.h

#ifndef FNV_H
#define FNV_H

#include <stddef.h>
#include <stdint.h>

/* 64-bit FNV-1a hash of `len` bytes starting at `data`. */
uint64_t fnv1a_64(const uint8_t *data, size_t len);

#endif

vendor/fnv/src/fnv.c

#include "fnv.h"

#define FNV_OFFSET_BASIS 0xcbf29ce484222325ULL
#define FNV_PRIME 0x100000001b3ULL

uint64_t fnv1a_64(const uint8_t *data, size_t len) {
    uint64_t hash = FNV_OFFSET_BASIS;
    for (size_t i = 0; i < len; i++) {
        hash ^= data[i];
        hash *= FNV_PRIME;
    }
    return hash;
}

src/main.rs

use std::error::Error;

unsafe extern "C" {
    fn fnv1a_64(data: *const u8, len: usize) -> u64;
}

/// Safe wrapper around the unsafe `fnv1a_64` C function.
fn fnv1a(data: &[u8]) -> u64 {
    unsafe { fnv1a_64(data.as_ptr(), data.len()) }
}

fn main() -> Result<(), Box<dyn Error>> {
    let input = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "hello".to_string());
    println!("fnv1a-64({input:?}) = {:016x}", fnv1a(input.as_bytes()));
    Ok(())
}

C++

cc Development tools

Linking a bundled C++ library is very similar to linking a bundled C library. The two core differences when compiling and statically linking a bundled C++ library are specifying a C++ compiler via the builder method cpp(true) and preventing name mangling by the C++ compiler by adding the extern "C" section at the top of our C++ source file.

build.rs

fn main() {
    cc::Build::new()
        .cpp(true)
        .file("src/foo.cpp")
        .compile("foo");
}

src/foo.cpp

extern "C" {
    int multiply(int x, int y);
}

int multiply(int x, int y) {
    return x*y;
}

src/main.rs

unsafe extern "C" {
    fn multiply(x: i32, y: i32) -> i32;
}

fn main() {
    unsafe {
        println!("{}", multiply(5, 7));
    }
}

std Development tools

For more complex scenarios, a rust project may need to integrate with a shared library that is dynamically linked at runtime instead of bundled into the binary. This is useful when embedding a system-level library that isn’t intended to be compiled into client applications directly, or a library that gets updated regularly outside the scope of your project. A mixture of build scripts (build.rs) and compiler directives teaches the compiler, and the final binary, how to find and link to the shared library.

This example does not use the cc crate to compile the shared library, since you often don’t have access to a shared library’s source or build process — you’re just consuming its binary directly. For the purpose of this example, the shared library’s source is in src/mylibrary.h and src/mylibrary.cc so its API surface is visible, and a Makefile builds it the way you would outside of the rust project’s own build process, independent of the cc crate.

Before compiling rust source code, the “build” file (build.rs) specified in Cargo.toml runs. It builds libmylibrary.so by invoking make, tells rustc where to find it at compile time with the cargo:rustc-link-search directive (pointed at Cargo’s own OUT_DIR, rather than a fixed path relative to the crate root), and enumerates the shared library dependency with the cargo:rustc-link-lib directive.

cargo:rustc-link-lib also bakes dynamic dependency metadata into the final binary, which tells the linker the name of the library to find at runtime. At runtime it would normally search default system locations (e.g. /usr/lib), which won’t contain libmylibrary.so since it was built into a Cargo-managed output directory. The cargo:rustc-link-arg directive bakes a runtime search path (an “rpath”) into the binary pointing at that OUT_DIR, so the example runs without manually setting LD_LIBRARY_PATH.

build.rs

use std::env;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    // Build `libmylibrary.so` the way you would outside of a Rust project:
    // with its own Makefile, independent of the `cc` crate.
    let status = Command::new("make")
        .current_dir(&manifest_dir)
        .env("OUT_DIR", &out_dir)
        .status()
        .expect("failed to run `make`; is a C++ toolchain installed?");
    assert!(status.success(), "failed to build libmylibrary.so");

    // Tell rustc where to find `libmylibrary` at compile time...
    println!("cargo:rustc-link-search=native={}", out_dir.display());
    // ...and bake a runtime search path into the binary, so the dynamically
    // linked library can be found without setting `LD_LIBRARY_PATH` by hand.
    println!("cargo:rustc-link-arg=-Wl,-rpath,{}", out_dir.display());
    println!("cargo:rustc-link-lib=dylib=mylibrary");

    println!("cargo:rerun-if-changed=src/mylibrary.cc");
    println!("cargo:rerun-if-changed=src/mylibrary.h");
    println!("cargo:rerun-if-changed=Makefile");
}

Makefile

CXX = g++
CXXFLAGS = -std=c++11 -Wall -Werror

all: $(OUT_DIR)/libmylibrary.so

$(OUT_DIR)/libmylibrary.so: src/mylibrary.cc src/mylibrary.h
	# Position-independent code (`-fpic`) is required for shared libraries.
	$(CXX) $(CXXFLAGS) -fpic -c src/mylibrary.cc -o $(OUT_DIR)/mylibrary.o
	$(CXX) -shared -o $(OUT_DIR)/libmylibrary.so $(OUT_DIR)/mylibrary.o
	rm $(OUT_DIR)/mylibrary.o

src/mylibrary.h

// Use C linkage for ABI stability.
extern "C" {

int add_numbers(int a, int b);
void greet(const char* name);

}  // extern "C"

src/mylibrary.cc

#include "mylibrary.h"

#include <iostream>

extern "C" {

int add_numbers(int a, int b) {
    return a + b;
}

void greet(const char* name) {
    std::cout << "Hello from a C++ shared library, " << name << "!" << std::endl;
}

}  // extern "C"

src/main.rs

use anyhow::Result;
use std::ffi::CString;
use std::os::raw::c_char;

unsafe extern "C" {
    fn add_numbers(a: i32, b: i32) -> i32;
    fn greet(name: *const c_char);
}

fn prompt(s: &str) -> Result<String> {
    use std::io::Write;
    print!("{s}");
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    Ok(input.trim().to_string())
}

// Safe wrapper around the unsafe `add_numbers` C function.
fn library_add_numbers(a: i32, b: i32) -> i32 {
    unsafe { add_numbers(a, b) }
}

// Safe wrapper around the unsafe `greet` C function.
fn library_greet(name: &CString) {
    unsafe { greet(name.as_ptr()) }
}

fn main() -> Result<()> {
    let name = prompt("What's your name? ")?;
    let c_name = CString::new(name)?;
    library_greet(&c_name);
    let result = library_add_numbers(10, 12);
    println!("Addition result: {result}");
    Ok(())
}