RustArena

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:

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

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