710: Calling C Functions with `extern "C"`
Difficulty: 4 Level: Expert Declare C function signatures, call them safely, and wrap the unsafe boundary in a clean Rust API.The Problem This Solves
The C ABI is the universal language of system interfaces. Every OS, database driver, cryptographic library, and hardware SDK exposes a C API. Rust must be able to call these libraries โ not by reimplementing them, but by linking against the existing binary and calling through the C ABI at runtime. `extern "C"` is the mechanism. You declare the function signatures Rust needs to know about, and the linker resolves the actual addresses at link time. The call itself happens at runtime. Because Rust's safety model cannot look inside a C function body โ C has no borrow checker โ every `extern "C"` call is unconditionally `unsafe`. You are responsible for: passing valid pointers, respecting ownership of any memory the C function returns, and matching the C function's actual type signature exactly. unsafe is a tool, not a crutch โ use only when safe Rust genuinely can't express the pattern.The Intuition
Think of `extern "C"` as a phone book entry: you declare the name and calling convention so Rust can generate the correct machine code, but the function itself lives in a different binary. Rust will place the arguments in the right registers and stack slots (per the C ABI), jump to the address, and trust that the C code does what the declaration says. If your declaration has the wrong types โ say `i32` where C expects `unsigned long` on a 64-bit platform โ the resulting call is undefined behaviour with no compiler error. Matching types precisely is the core discipline of FFI.How It Works in Rust
use std::os::raw::c_int;
// โโ Simulated C library (Rust with C ABI) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[no_mangle]
pub extern "C" fn c_add(a: c_int, b: c_int) -> c_int { a + b }
#[no_mangle]
pub extern "C" fn c_clamp(n: c_int, lo: c_int, hi: c_int) -> c_int {
if n < lo { lo } else if n > hi { hi } else { n }
}
// โโ Safe wrappers โ validate before crossing the FFI boundary โโโโโโโโโโโ
pub fn safe_add(a: i32, b: i32) -> i32 {
unsafe {
// SAFETY: c_add is pure integer addition; no preconditions beyond
// valid i32 values, which Rust's type system guarantees.
c_add(a, b)
}
}
pub fn safe_clamp(n: i32, lo: i32, hi: i32) -> Result<i32, String> {
if lo > hi {
return Err(format!("lo ({lo}) > hi ({hi})"));
}
Ok(unsafe {
// SAFETY: lo <= hi (checked above); all i32 values are valid inputs.
c_clamp(n, lo, hi)
})
}
Use `std::os::raw` types (`c_int`, `c_char`, `c_void`, etc.) rather than Rust primitives when declaring `extern "C"` signatures โ their sizes match the C ABI on every target platform.
What This Unlocks
- System programming โ call `libc`, OS APIs, and hardware drivers that expose C interfaces (memory allocation, socket operations, GPU compute).
- Cryptography and performance libraries โ OpenSSL, libsodium, BLAS, and similar libraries expose C APIs that Rust doesn't need to reinvent.
- Gradual migration โ call existing C/C++ codebases from new Rust code while incrementally replacing modules.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| C FFI declaration | `external my_fn : int -> int = "my_fn"` | `extern "C" { fn my_fn(x: c_int) -> c_int; }` |
| Safety of C calls | Unchecked by default | Always `unsafe` โ must write `unsafe { }` |
| Type mapping | OCaml `int` โ C `int` (tag bit) | `c_int` from `std::os::raw` โ exact C ABI types |
| Linking | `(-cclib -lm)` in dune | `#[link(name = "m")]` or build.rs `println!("cargo:rustc-link-lib=m")` |
| Safe wrapper idiom | Typically not enforced | Strong convention: `unsafe fn` โ `pub fn` wrapper |
//! 710 โ Calling C Functions with extern "C"
//! Simulate C library with #[no_mangle] exports, call via extern "C".
use std::os::raw::c_int;
// โโ "C library" side: implemented in Rust, exported with C ABI โโโโโโโโโโโ
#[no_mangle]
pub extern "C" fn c_add(a: c_int, b: c_int) -> c_int { a + b }
#[no_mangle]
pub extern "C" fn c_abs(n: c_int) -> c_int { n.abs() }
#[no_mangle]
pub extern "C" fn c_max(a: c_int, b: c_int) -> c_int { if a > b { a } else { b } }
#[no_mangle]
pub extern "C" fn c_clamp(n: c_int, lo: c_int, hi: c_int) -> c_int {
if n < lo { lo } else if n > hi { hi } else { n }
}
// โโ "Rust caller" side: extern "C" declarations โโโโโโโโโโโโโโโโโโโโโโโโโโโ
// c_add, c_abs, c_max, c_clamp defined above โ call directly.
/// Safe wrappers โ validate inputs before crossing the FFI boundary.
pub fn safe_add(a: i32, b: i32) -> i32 {
unsafe {
// SAFETY: c_add is pure integer addition; no preconditions.
c_add(a, b)
}
}
pub fn safe_abs(n: i32) -> i32 {
unsafe {
// SAFETY: c_abs accepts any i32.
c_abs(n)
}
}
pub fn safe_max(a: i32, b: i32) -> i32 {
unsafe {
// SAFETY: c_max accepts any two i32 values.
c_max(a, b)
}
}
pub fn safe_clamp(n: i32, lo: i32, hi: i32) -> Result<i32, String> {
if lo > hi {
return Err(format!("lo ({lo}) > hi ({hi})"));
}
Ok(unsafe {
// SAFETY: lo <= hi (checked above); all i32 values are valid.
c_clamp(n, lo, hi)
})
}
fn main() {
println!("c_add(3, 4) = {}", safe_add(3, 4));
println!("c_abs(-7) = {}", safe_abs(-7));
println!("c_max(10, 20) = {}", safe_max(10, 20));
println!("c_clamp(5, 0, 3) = {:?}", safe_clamp(5, 0, 3));
println!("c_clamp(2, 0, 3) = {:?}", safe_clamp(2, 0, 3));
println!("c_clamp(5, 10, 0) = {:?}", safe_clamp(5, 10, 0)); // Err
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() { assert_eq!(safe_add(2, 3), 5); assert_eq!(safe_add(-5, 5), 0); }
#[test]
fn test_abs() { assert_eq!(safe_abs(-42), 42); assert_eq!(safe_abs(0), 0); }
#[test]
fn test_max() { assert_eq!(safe_max(1, 2), 2); assert_eq!(safe_max(5, 3), 5); }
#[test]
fn test_clamp_valid() {
assert_eq!(safe_clamp(5, 0, 10).unwrap(), 5);
assert_eq!(safe_clamp(-1, 0, 10).unwrap(), 0);
assert_eq!(safe_clamp(15, 0, 10).unwrap(), 10);
}
#[test]
fn test_clamp_invalid() {
assert!(safe_clamp(5, 10, 0).is_err());
}
}
(* OCaml: external declarations link to C symbols via the OCaml runtime. *)
(* In real OCaml, you'd write: external c_add : int -> int -> int = "c_add" *)
(* Here we simulate the C functions directly in OCaml. *)
let c_add (a : int) (b : int) : int = a + b
let c_abs (n : int) : int = if n < 0 then -n else n
let c_max (a : int) (b : int) : int = if a > b then a else b
let () =
Printf.printf "c_add(3, 4) = %d\n" (c_add 3 4);
Printf.printf "c_abs(-7) = %d\n" (c_abs (-7));
Printf.printf "c_max(10, 20) = %d\n" (c_max 10 20)