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.
Compile and link statically to a bundled C library
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 and link statically to a bundled C++ library
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
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();
}
}
Compile and link dynamically to a C++ shared library
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(())
}