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.

cc-badge cat-development-tools-badge

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(())
}

cc-badge cat-development-tools-badge

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));
    }
}

Compile a C library while setting custom defines

cc-badge cat-development-tools-badge

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-badge cat-development-tools-badge

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(())
}