Rust was originally designed as a systems language for writing high performance applications that are usually written in C or C++. Although Rust solves problems that C/C++ developers have been struggling with for a long time there are countless amount of C and C++ libraries and it makes no sense to replace all of them with Rust. For this reason, Rust makes it easy to communicate with C APIs without overhead, and to leverage its ownership system to provide much stronger safety guarantees for those APIs at the same time.
Rust provides a foreign function interface (FFI) to communicate with other languages. Following Rust's design principles, the FFI provides a zero-cost abstraction where function calls between C and Rust have identical performance to C function calls. FFI bindings can also leverage language features such as ownership and borrowing to provide a safe interface that enforces protocols around pointers and other resources.
In this post we'll explore how to call C code from Rust and to build C and Rust code together as well as how to automate this process. You can find the source code of the complete project on github.
Note: As C++ has no stable ABI for the Rust compiler to target, it is recommended to use C ABI for any interoperability between Rust and C++. Therefore, in this article we will talk about interacting with C code, because the concepts provided here cover both C and C++.
Creating bindings
We will start with a simple example of calling C code from Rust. Let's imagine that we have the following header file:
/* File: libvec/vec.h */
#pragma once
typedef struct Vec2 {
int x;
int y;
} Vec2;
void scale(Vec2* vec, int factor);
and corresponding C file:
/* File: libvec/vec.c */
#include "vec.h"
void scale(Vec2* vec, int factor) {
vec->x *= factor;
vec->y *= factor;
}
Before we start using any C code from Rust, we need to define all data types and function signatures we are going to interact with in Rust. Usually in C/C++ world we use header files for that purpose, which define all necessary data. But in Rust, we need to either manually translate these definitions to Rust code, or use a tool to generate these definitions.
Let us write a rust file which resembles the C header file:
/* File: src/bindings.rs */
use std::os::raw::c_int;
#[repr(C)]
#[derive(Debug)]
pub struct Vec2 {
pub x: c_int,
pub y: c_int,
}
extern "C" {
pub fn scale(
vec: *mut Vec2,
factor: c_int
);
}
Rust needs to laydown members in a structure the way C would laydown it in memory. For that reason we include the #[repr(C)]
attribute, which instructs the Rust compiler to always use the same rules C does for organizing data within a struct.
We declared function scale
without defining a body. So, we use extern
keyword to indicate the fact that the definition of this function will be provided elsewhere or linked into the final library or binary from a static library.
In the extern
block, we have a string, "C"
, following it. This "C"
specifies that we want the compiler to respect the C ABI so that the function-calling convention follows exactly as a function call that's done from C. An Application Binary Interface(ABI) is basically a set of rules and conventions that dictates how types and functions are represented and manipulated at the lower levels. There are also other ABIs that Rust supports such as fastcall
, cdecl
, win64
, and others, but this topic is beyond the scope of the current article.
Generating bindings
So far we manually wrote interface to interact with C. But this approach has some drawbacks. Consider real case scenario where each header file has lots of structure definitions, function declarations, inline functions etc. As well as writing interfaces manually is tedious and error prone. But luckily for us there is a tool called bindgen which can perform these conversions automatically.
We can install bindgen
with the following command:
$ cargo install bindgen
The command to generate bindings may look like this:
$ bindgen libvec/vec.h -o src/bindings.rs
Although we can use bindgen
as a command line tool because it seems to give you more control over generation, the recommended way to use bindgen
is generating the bindings on the fly through build.rs
script.
A build.rs
script is a file written in Rust syntax, that is executed on your compilation machine, after dependencies of your project have been built, but before your project is built. If you want to know more about build.rs
please refer to the documentation. To generate bindings with bindgen
we add the build.rs
file with the following content:
/* File: build.rs */
extern crate bindgen;
use std::path::PathBuf;
fn main() {
// Tell cargo to invalidate the built crate whenever the wrapper changes
println!("cargo:rerun-if-changed=libvec/vec.h");
let bindings = bindgen::Builder::default()
// The input header we would like to generate bindings for.
.header("libvec/vec.h")
// Tell cargo to invalidate the built crate whenever any of the
// included header files changed.
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
// Finish the builder and generate the bindings.
.generate()
.expect("Unable to generate bindings");
// Write the bindings to the src/bindings.rs file.
let out_path = PathBuf::from("src");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}
We also need to add bindgen
to build dependencies in Cargo.toml
file:
[build-dependencies]
bindgen = "0.53.1"
Now, when we run cargo build
, our bindings are generated on the fly to src/bindings.rs
file.
Finally, we can use our generated buildings in Rust as in the following example:
/* File: src/main.rs */
use crate::bindings::{scale, Vec2};
mod bindings;
fn main() {
unsafe{
let mut vec = Box::new(Vec2{ x: 5, y: 10 });
scale(&mut *vec, 2);
println!("scaled vector: {:?}", *vec);
}
}
Building C code
Since the Rust compiler does not directly know how to compile C code, we need to compile our non-Rust code ahead of time. We can start simple and compile it in static library, which then will be combined with our Rust code at the final linking step.
To compile static library libvec.a
from our C code we can use the following command:
$ cc -c libvec/vec.c
$ ar rcs libvec.a vec.o
Now we can link it all together:
$ rustc -l static=vec -L. src/main.rs
This command tells Rust compiler to look for a static libvec.a
in the current .
path. This should compile and give you the main binary, which can be executed.
Using cc
crate
It might be quite tedious to compile static library manually every time we make changes in C code. The better solution is to instead utilize the cc crate, which provides an idiomatic Rust interface to the compiler provided by the host.
In our case of compiling a single C file as a dependency to a static library, we can amend out build.rs
script using the cc
crate. The modified version would look like this:
/* File: build.rs */
extern crate bindgen;
extern crate cc;
use std::path::PathBuf;
fn main() {
// Tell cargo to invalidate the built crate whenever the wrapper changes
println!("cargo:rerun-if-changed=libvec/vec.h");
let bindings = bindgen::Builder::default()
// The input header we would like to generate bindings for.
.header("libvec/vec.h")
// Tell cargo to invalidate the built crate whenever any of the
// included header files changed.
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
// Finish the builder and generate the bindings.
.generate()
.expect("Unable to generate bindings");
// Write the bindings to the src/bindings.rs file.
let out_path = PathBuf::from("src");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
// Build static library
cc::Build::new()
.file("libvec/vec.c")
.compile("libvec.a");
}
Updated build dependencies section in Cargo.toml
would look like this:
[build-dependencies]
bindgen = "0.53.1"
cc = "1.0"
Now we can simply run cargo build
to generate bindings, compile static library and link it with a Rust code. How cool is that!
Using CMake
In real world we usually have existing C projects that often use popular build systems like make
or CMake
. And it would be very inconvenient to use cc
crate to compile them and don't have an option to reuse existing makefiles. Likely for us Rust have cmake crate that is intended to help us.
Let's assume that our vec
library uses CMake
and we have the following CMakeLists.txt
file:
# File: libvec/CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(libvec C)
add_library(vec STATIC vec.c)
install(TARGETS vec DESTINATION .)
To use cmake
crate we need to add build dependency on it to Cargo.toml
file:
[build-dependencies]
bindgen = "0.53.1"
cmake = "0.1.48"
And modify our build.rs
file as follows:
/* File: build.rs */
extern crate bindgen;
extern crate cmake;
use cmake::Config;
use std::path::PathBuf;
fn main() {
// Tell cargo to invalidate the built crate whenever the wrapper changes
println!("cargo:rerun-if-changed=libvec/vec.h");
let bindings = bindgen::Builder::default()
// The input header we would like to generate bindings for.
.header("libvec/vec.h")
// Tell cargo to invalidate the built crate whenever any of the
// included header files changed.
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
// Finish the builder and generate the bindings.
.generate()
.expect("Unable to generate bindings");
// Write the bindings to the src/bindings.rs file.
let out_path = PathBuf::from("src");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
// Build static library
let dst = Config::new("libvec").build();
println!("cargo:rustc-link-search=native={}", dst.display());
println!("cargo:rustc-link-lib=static=vec");
}
We use cmake::Config
type to trigger CMake
driven to build our library, telling that it’s code and CMakeFiles.txt
files are located in libvec
subdirectory and then requesting the build to happen. Next lines write to stdout special command for Cargo to set library search path and pick our libvec.a
for linking respectively. After that we can compile our project with cargo build
.
Summary
We've talked today about Rust and C interoperability, how to generate bindings to C code and how to automate this process as well as how to use cc
and CMake
crates to build C code from cargo.
I hope you have found the information above useful and helpful, any questions and feedbacks and welcomed.
If you find this article interesting don't forget to like it and give a start to source code.
Top comments (4)
What is the output of the
command?
Is it executable or is it an object file? If it is an object file, what is the format of it?
It is possible to compile multiple rs files and link them to executable without using cargo?
This command outputs executable.
rustc
has an option--emit obj
to output an object file. I believe the format of object file depends on platform. On macOS it's Mach-O format.Compilation unit in rust is crate, not file, so It seems possible to compile multiple crates and link them in executable with standard linker (like ld), but you also need to link rust std somehow (if you use it).
Hi, Great article! I'have made a similar one about writing python modules in rust, would be great if you take a look and give a feedback what you thing about it, thanks! ❤️
cargo install bindgen
fails.Use
cargo install bindgen-cli
instead to install the bindgen binary