extern c functions
Tutorial
The Problem
This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming — writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.
🎯 Learning Outcomes
Code Example
use std::os::raw::c_int;
extern "C" {
fn c_add(a: c_int, b: c_int) -> c_int;
fn c_abs(n: c_int) -> c_int;
fn c_max(a: c_int, b: c_int) -> c_int;
}Key Differences
unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.extern "C"; OCaml uses ctypes which wraps C types in OCaml values.#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.OCaml Approach
OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:
ctypes library for external function callsBigarray for controlled raw memory access Bytes.t for mutable byte sequencesOCaml programs rarely need operations equivalent to these Rust unsafe patterns.
Full Source
#![allow(clippy::all)]
//! 710 — Calling C Functions with `extern "C"`
//!
//! Pattern: `#[no_mangle] pub extern "C"` exports a Rust fn with C ABI.
//! `extern "C" { fn name(...); }` imports an external C symbol.
//! Safe wrappers isolate `unsafe` at the FFI boundary.
//!
//! In a real project the C-side functions live in a compiled `.a`/`.so`
//! and are linked via a `build.rs` + `println!("cargo:rustc-link-lib=...")`.
//! Here we implement them in Rust with the C calling convention so the
//! example is fully self-contained and testable without a C compiler.
use std::os::raw::c_int;
// ── Simulated C library ───────────────────────────────────────────────────
// `#[no_mangle]` emits the symbol with the bare name (no Rust mangling)
// so the linker can match it to the `extern "C"` declaration below.
#[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 {
a.max(b)
}
#[no_mangle]
pub extern "C" fn c_clamp(n: c_int, lo: c_int, hi: c_int) -> c_int {
n.clamp(lo, hi)
}
// ── FFI declarations ──────────────────────────────────────────────────────
// This `extern "C"` block is what you write when calling a real C library.
// The linker resolves each declaration to the compiled C symbol at link time.
mod ffi {
use std::os::raw::c_int;
extern "C" {
pub fn c_add(a: c_int, b: c_int) -> c_int;
pub fn c_abs(n: c_int) -> c_int;
pub fn c_max(a: c_int, b: c_int) -> c_int;
pub fn c_clamp(n: c_int, lo: c_int, hi: c_int) -> c_int;
}
}
// ── Safe wrappers ─────────────────────────────────────────────────────────
// `unsafe` is quarantined here. Every precondition is validated before the
// FFI call so callers get a safe, idiomatic Rust API.
/// Add two integers through the C ABI.
pub fn safe_add(a: i32, b: i32) -> i32 {
// SAFETY: c_add reads two ints and returns their sum.
// No pointers; no undefined behaviour.
unsafe { ffi::c_add(a, b) }
}
/// Absolute value through the C ABI.
pub fn safe_abs(n: i32) -> i32 {
// SAFETY: c_abs reads one int. Our implementation uses Rust's .abs()
// which is defined for all i32 values (wrapping on MIN in debug is
// prevented by the Rust semantics of the #[no_mangle] body above).
unsafe { ffi::c_abs(n) }
}
/// Maximum of two integers through the C ABI.
pub fn safe_max(a: i32, b: i32) -> i32 {
// SAFETY: c_max reads two ints. No pointers, no UB.
unsafe { ffi::c_max(a, b) }
}
/// Clamp `n` to `[lo, hi]`. Returns `None` when `lo > hi`.
///
/// Input validation before the FFI call is the idiomatic pattern for
/// expressing Rust's safety contracts at an `extern "C"` boundary.
pub fn safe_clamp(n: i32, lo: i32, hi: i32) -> Option<i32> {
if lo > hi {
return None;
}
// SAFETY: c_clamp reads three ints. lo <= hi is established above.
Some(unsafe { ffi::c_clamp(n, lo, hi) })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_basic() {
assert_eq!(safe_add(3, 4), 7);
assert_eq!(safe_add(0, 0), 0);
assert_eq!(safe_add(-5, 5), 0);
assert_eq!(safe_add(-3, -4), -7);
}
#[test]
fn test_abs_positive_and_negative() {
assert_eq!(safe_abs(0), 0);
assert_eq!(safe_abs(7), 7);
assert_eq!(safe_abs(-7), 7);
assert_eq!(safe_abs(i32::MAX), i32::MAX);
}
#[test]
fn test_max_ordering() {
assert_eq!(safe_max(10, 20), 20);
assert_eq!(safe_max(20, 10), 20);
assert_eq!(safe_max(5, 5), 5);
assert_eq!(safe_max(-1, -2), -1);
}
#[test]
fn test_clamp_within_range() {
assert_eq!(safe_clamp(5, 0, 10), Some(5));
assert_eq!(safe_clamp(0, 0, 10), Some(0));
assert_eq!(safe_clamp(10, 0, 10), Some(10));
}
#[test]
fn test_clamp_out_of_range() {
assert_eq!(safe_clamp(-1, 0, 10), Some(0));
assert_eq!(safe_clamp(15, 0, 10), Some(10));
}
#[test]
fn test_clamp_invalid_range_returns_none() {
assert_eq!(safe_clamp(5, 10, 0), None);
assert_eq!(safe_clamp(5, 1, 0), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_basic() {
assert_eq!(safe_add(3, 4), 7);
assert_eq!(safe_add(0, 0), 0);
assert_eq!(safe_add(-5, 5), 0);
assert_eq!(safe_add(-3, -4), -7);
}
#[test]
fn test_abs_positive_and_negative() {
assert_eq!(safe_abs(0), 0);
assert_eq!(safe_abs(7), 7);
assert_eq!(safe_abs(-7), 7);
assert_eq!(safe_abs(i32::MAX), i32::MAX);
}
#[test]
fn test_max_ordering() {
assert_eq!(safe_max(10, 20), 20);
assert_eq!(safe_max(20, 10), 20);
assert_eq!(safe_max(5, 5), 5);
assert_eq!(safe_max(-1, -2), -1);
}
#[test]
fn test_clamp_within_range() {
assert_eq!(safe_clamp(5, 0, 10), Some(5));
assert_eq!(safe_clamp(0, 0, 10), Some(0));
assert_eq!(safe_clamp(10, 0, 10), Some(10));
}
#[test]
fn test_clamp_out_of_range() {
assert_eq!(safe_clamp(-1, 0, 10), Some(0));
assert_eq!(safe_clamp(15, 0, 10), Some(10));
}
#[test]
fn test_clamp_invalid_range_returns_none() {
assert_eq!(safe_clamp(5, 10, 0), None);
assert_eq!(safe_clamp(5, 1, 0), None);
}
}
Deep Comparison
OCaml vs Rust: Calling C Functions with extern "C"
Side-by-Side Code
OCaml — external declaration
(* OCaml binds to C symbols with `external`. *)
external c_add : int -> int -> int = "c_add"
external c_abs : int -> int = "c_abs"
external c_max : int -> int -> int = "c_max"
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)
Rust — extern "C" declaration
use std::os::raw::c_int;
extern "C" {
fn c_add(a: c_int, b: c_int) -> c_int;
fn c_abs(n: c_int) -> c_int;
fn c_max(a: c_int, b: c_int) -> c_int;
}
Rust — safe wrappers (idiomatic boundary pattern)
pub fn safe_add(a: i32, b: i32) -> i32 {
// SAFETY: no pointers, no aliasing, no UB for any i32 pair.
unsafe { ffi::c_add(a, b) }
}
pub fn safe_clamp(n: i32, lo: i32, hi: i32) -> Option<i32> {
if lo > hi { return None; }
// SAFETY: lo <= hi validated above.
Some(unsafe { ffi::c_clamp(n, lo, hi) })
}
Rust — C-side simulation (#[no_mangle])
#[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 { a.max(b) }
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| FFI declaration | external c_add : int -> int -> int = "c_add" | extern "C" { fn c_add(a: c_int, b: c_int) -> c_int; } |
| C integer type | int (OCaml int, not C int!) | c_int = i32 on all common platforms |
| Unsafe marker | Implicit — OCaml trusts the stub | Explicit unsafe {} block at every call |
| Safe wrapper | Optional (OCaml has no unsafe) | Idiomatic — isolate unsafe in one place |
| Export for C | Automatic via C stub generator | #[no_mangle] pub extern "C" fn |
Key Insights
external name : type = "c_symbol" — a single line that names the OCaml identifier, gives its type, and maps it to the C symbol name. Rust separates these concerns: extern "C" { fn name(...); } declares the import, and the linker resolves the symbol name.unsafe keyword — calling C via external is syntactically identical to calling OCaml code, leaving safety verification entirely to the programmer. Rust makes the danger explicit: every call through an extern "C" declaration requires an unsafe {} block, forcing you to document why each call is sound.int is a 63-bit tagged integer (not a C int), so naive type-pun between OCaml int and C int is a bug; OCaml's C stub system handles the conversion automatically. Rust's c_int is exactly C's int (an alias for i32 on all mainstream platforms), so the mapping is direct and explicit.extern "C" call in a safe public function that validates preconditions before crossing the boundary. OCaml achieves the same with regular functions that check arguments, but there is no language-level distinction between the unsafe FFI call and the validation wrapper.#[no_mangle] pub extern "C" fn to implement the "C library" side in Rust with C calling convention, so no C compiler is needed. The linker resolves the extern "C" declarations to the #[no_mangle] definitions within the same binary — the same mechanism that resolves them to a real .so in production.When to Use Each Style
**Use extern "C" with safe wrappers when:** integrating OS APIs, database drivers, cryptographic libraries, or any existing C/C++ codebase — the safe wrapper is the idiomatic Rust boundary.
**Use #[no_mangle] pub extern "C" fn when:* you need Rust code to be callable* from C, Python (via ctypes), or other languages — you are the library author, not the consumer.
Exercises
bytemuck for transmute, CString for FFI strings) and implement it.