FFI: Linking Rust and C or C++
Rust behaves very well when talking to other languages---both as a library for other languages to consume, and as a consumer of other languages' libraries.
We'll refer to "C Libraries"---but we really mean any language that compiles to a C-friendly library format. C, C++, Go, Fortran, Haskell, and many others can all be consumed by Rust.
Consuming C Libraries
The code for this is in
04_mem/c_rust
(C Rust)
Let's start with a tiny C library:
// A simple function that doubles a number
int double_it(int x) {
return x * 2;
}
We'd like to compile this and include it in a Rust program. We can automate compilation by including the ability to compile C (and C++) libraries as part of our build process with the cc
crate. Rather than adding it with cargo add
, we want to add it as a build dependency. It won't be included in the final program, it's just used during compilation. Open Cargo.toml
:
[package]
name = "c_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
[build-dependencies]
cc = "1"
Now we can create a build.rs
file in the root of our project (not the src
directory). This file will be run as part of the build process, and can be used to compile C libraries. We'll use the cc
crate to do this:
fn main() { cc::Build::new() .file("src/crust.c") .compile("crust"); }
build.rs
is automatically compiled and executed when your Rust program builds. You can use it to automate any build-time tasks you want. The cc
calls will build the listed files and include the linked result in your final program as a static library.
Lastly, let's create some Rust to call the C:
#![allow(unused)] fn main() { // Do it by hand extern "C" { fn double_it(x: i32) -> i32; } mod rust { pub fn double_it(x: i32) -> i32 { x * 2 } } }
We've used an extern "C"
to specify linkage to an external C library. We've also created a Rust version of the same function, so we can compare the two.
Now let's use some unit tests to prove that it works:
#![allow(unused)] fn main() { #[cfg(test)] mod test { use super::*; #[test] fn test_double_it() { assert_eq!(unsafe { double_it(2) }, 4); } #[test] fn test_c_rust() { assert_eq!(unsafe { double_it(2) }, rust::double_it(2)); } } }
And it works when we run cargo test
.
Header files and BindGen
You need LLVM installed (clang 5 or greater) to use this. On Windows,
winget install LLVM.LLVM
will work. Also set an environment variableLIBCLANG_PATH
to the location of the Clang install. On Windows,$Env:LIBCLANG_PATH="C:\Program Files\LLVM\bin"
Larger C examples will include header files. Let's add crust.h
:
int double_it(int x);
And add C to require it:
#include "crust.h"
// A simple function that doubles a number
int double_it(int x) {
return x * 2;
}
We can add it to the build.rs
file, but it will be ignored (it's just a forward declaration).
Writing the extern "C"
for a large library could be time consuming. Let's use bindgen
to do it for us.
Add another build-dependency:
[build-dependencies]
cc = "1"
bindgen = "0"
Now in build.rs
we'll add some calls to use it:
#![allow(unused)] fn main() { let bindings = bindgen::Builder::default() .header("src/crust.h") .parse_callbacks(Box::new(bindgen::CargoCallbacks)) .generate() .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!"); }
This is pretty much standard boilerplate, but there are a lot of options available.
Now run cargo build
. You'll see a new file in target/debug/build/c_rust-*/out/bindings.rs
. This is the automatically generated bindings file. Let's use it:
#![allow(unused)] fn main() { include!(concat!(env!("OUT_DIR"), "/bindings.rs")); }
Your compile time has suffered, but now the header is parsed and Rust bindings are generated automatically. The unit tests should still work.
Calling Rust from Other Languages
The code for this is in
04_mem/rust_c
(Rust C)
You can also setup Rust functions and structures for export via a C API. You lose some of the richness of the Rust language ---everything has to be C compatible---but you can still use Rust's safety and performance.
Start with some Cargo.toml
entries:
[package]
name = "rust_c"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"]
[dependencies]
libc = "0.2"
Providing a lib
and crate-type
section lets you change compilation behavior. We're instructing Rust to build a C-compatible static library (it can also take a dynlib
for dynamic linkage).
Next, we'll build a single Rust function to export:
#![allow(unused)] fn main() { use std::ffi::CStr; /// # Safety /// Use a valid C-String! #[no_mangle] pub unsafe extern "C" fn hello(name: *const libc::c_char) { let name_cstr = unsafe { CStr::from_ptr(name) }; let name = name_cstr.to_str().unwrap(); println!("Hello {name}"); } }
Notice that we're using c_char
as an array---just like the C ABI. CStr
and CString
provide Rust friendly layers between string types, allowing you to convert back and forth. C strings will never be as safe as Rust strings, but this is a good compromise.
We've turned off name mangling, making it easy for the linker to find the function.
The function is also "unsafe"---because it receives an unsafe C string type.
Build the project with cargo build
, and you'll see that target/debug/rust_c.lib
(on Windows, .a
on Linux) has been created. This is the static library that we can link to from C.
Linkage via C requires a header file. In this case, it's pretty easy to just write one:
void hello(char *name);
You can now use this in C or another language. In Go, it looks like this:
package main
/*
#cgo LDFLAGS: ./rust_c.a -ldl
#include "./lib/rust_c.h"
*/
import "C"
import "fmt"
import "time"
func main() {
start := time.Now()
fmt.Println("Hello from GoLang!")
duration := time.Since(start)
fmt.Println(duration)
start2 := time.Now()
C.hello(C.CString("from Rust!"))
duration2 := time.Since(start2)
fmt.Println(duration2)
}
(There's a few microseconds delay in the Rust call, but it's pretty fast! Marshaling the C string in Go is the slowest part).
Using CBindGen to Write the Header For You
Setup cbindgen
as a build dependency:
[package]
name = "rust_c"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"]
[dependencies]
libc = "0.2"
[build-dependencies]
cbindgen = "0.24"
And once again, add a build.rs
file:
use std::env; use std::path::PathBuf; use cbindgen::Config; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let package_name = env::var("CARGO_PKG_NAME").unwrap(); let output_file = target_dir() .join(format!("{}.hpp", package_name)) .display() .to_string(); let config = Config { //namespace: Some(String::from("ffi")), ..Default::default() }; cbindgen::generate_with_config(&crate_dir, config) .unwrap() .write_to_file(&output_file); } /// Find the location of the `target/` directory. Note that this may be /// overridden by `cmake`, so we also need to check the `CARGO_TARGET_DIR` /// variable. fn target_dir() -> PathBuf { if let Ok(target) = env::var("CARGO_TARGET_DIR") { PathBuf::from(target) } else { PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("target") } }
This is boilerplate from this guide
Now run cargo build
and a target
directory appears - with a header file.