Step 3 of 6
Calling C with FFI
A foreign function interface (FFI) lets Rust call functions written in other languages, almost always through the C calling convention (the C ABI). It's how Rust uses C libraries like SQLite, OpenSSL or zlib, calls operating system APIs, and how Python or C programs call into Rust.
On Linux, the C standard library is always linked into Rust programs, so its functions are ready to declare and call:
use std::ffi::{c_char, c_int, CStr, CString};
unsafe extern "C" {
fn abs(x: c_int) -> c_int;
fn strlen(s: *const c_char) -> usize;
}
fn main() {
// SAFETY: C's abs is defined for every int except INT_MIN; -42 is fine.
let a = unsafe { abs(-42) };
println!("abs(-42) = {a}");
let name = CString::new("Ferris").expect("no NUL bytes inside");
// SAFETY: name.as_ptr() is a NUL-terminated string that lives until the
// end of main, and strlen only reads it.
let n = unsafe { strlen(name.as_ptr()) };
println!("strlen = {n}");
// From C to Rust: view NUL-terminated bytes as a CStr, then as a &str.
let c = CStr::from_bytes_with_nul(b"crab\0").expect("exactly one NUL, at the end");
println!("{:?} -> {}", c, c.to_str().unwrap());
}
abs(-42) = 42
strlen = 6
"crab" -> crab
Piece by piece:
unsafe extern "C" { ... }declares functions that exist somewhere else and use the C ABI. The compiler can't check these signatures against the C code, so the block itself isunsafe: by writing it, you vouch that they're right. (Since edition 2024 theunsafeis required; plainextern "C" {fails withextern blocks must be unsafe.)- Calling a foreign function is unsafe too: the compiler can't know what the C code does with your arguments.
- C types have Rust names in
std::ffi:c_int,c_char,c_double, and so on. C'ssize_tisusize. - C strings are a pointer to bytes ending in a NUL (
\0) byte. Rust strings store their length instead and have no NUL.CStringmakes an owned, NUL-terminated copy of a Rust string (CString::newfails if the text contains a\0), and.as_ptr()gives C the*const c_charit wants.CStris the borrowed view in the other direction, for strings C gives you. b"crab\0"is a byte string literal: a&[u8; 5]holding the bytes of the text, here with a NUL byte written as\0at the end.
If a foreign function really is safe for every possible argument, you can declare it safe fn inside the block and call it without unsafe. abs doesn't qualify: in C, abs(INT_MIN) is undefined behavior, because the positive value doesn't fit in an int.
Common mistakes
Letting the CString die too early. .as_ptr() borrows from the CString, but raw pointers don't keep anything alive:
let p = CString::new("hi").unwrap().as_ptr(); // the CString is dropped right here
let n = unsafe { strlen(p) }; // reads freed memory: UB
The compiler catches this common case with a warning: this creates a dangling pointer because temporary `CString` is dropped at end of statement. Bind the CString to a variable first.
Passing a Rust string straight to C. "hello".as_ptr() points to bytes with no NUL at the end, so strlen keeps reading past the string into whatever memory follows. Always go through CString.
Getting the signature wrong. Declaring fn abs(x: i64) -> i64 for C's int abs(int) compiles fine and then passes the wrong number of bytes. Nothing checks it for you.
FFI at work
- The
libccrate already declares the whole C library for every platform; use it instead of writing declarations by hand. bindgengenerates theexterndeclarations from a C header file, and thecccrate compiles C code from abuild.rsscript.#[link(name = "z")]on an extern block links a system library.- Going the other way,
#[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32exports a Rust function C can call.cbindgenwrites the C header for it, and PyO3 exposes Rust functions to Python. - The usual shape is a small
-syscrate with the raw declarations, plus a safe wrapper crate on top: exactly the safe-abstraction pattern from the last step.
Your task
Write c_len, which measures a Rust string with C's strlen (declared for you). Convert it with CString::new; if the text contains a NUL byte, return None. Don't use Rust's .len().
Challenge 2
Write a sound safe wrapper around C's abs. As you read above, abs(INT_MIN) is undefined behavior in C, so the wrapper must never pass i32::MIN to it: return None for that input, and Some(abs(x)) otherwise. Call the C function (no Rust .abs() methods).
Challenge 3
Write the FFI declaration yourself this time. Declare C's int strcmp(const char *a, const char *b) in an unsafe extern "C" block, then write c_compare(a, b), which compares two Rust strings with it and returns an Ordering (strcmp returns a negative number, zero or a positive number). Return None if either string contains a NUL byte.
Previous: Unsafe functions and safe abstractions Next: Global state without static mut