ExamplesBy LevelBy TopicLearning Paths
710 Fundamental

extern c functions

Functional Programming

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

  • • The specific unsafe feature demonstrated: extern c functions
  • • When this feature is necessary vs when safe alternatives exist
  • • How to use it correctly with appropriate SAFETY documentation
  • • The invariants that must be maintained for the operation to be sound
  • • Real-world contexts: embedded systems, OS kernels, C FFI, performance-critical code
  • 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

  • Safety model: Rust requires explicit unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.
  • FFI approach: Rust uses raw C types directly with extern "C"; OCaml uses ctypes which wraps C types in OCaml values.
  • Memory control: Rust allows complete control over memory layout (#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.
  • Auditability: Rust unsafe regions are syntactically visible and toolable; OCaml unsafe operations (Obj.magic, direct C calls) are also explicit but less common.
  • OCaml Approach

    OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:

  • • C FFI via the ctypes library for external function calls
  • Bigarray for controlled raw memory access
  • • The GC for memory management (no manual allocators needed)
  • Bytes.t for mutable byte sequences
  • OCaml 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    FFI declarationexternal c_add : int -> int -> int = "c_add"extern "C" { fn c_add(a: c_int, b: c_int) -> c_int; }
    C integer typeint (OCaml int, not C int!)c_int = i32 on all common platforms
    Unsafe markerImplicit — OCaml trusts the stubExplicit unsafe {} block at every call
    Safe wrapperOptional (OCaml has no unsafe)Idiomatic — isolate unsafe in one place
    Export for CAutomatic via C stub generator#[no_mangle] pub extern "C" fn

    Key Insights

  • Declaration syntax: OCaml uses 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.
  • Safety model: OCaml has no 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.
  • Type mismatch risk: OCaml's 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.
  • Safe wrapper pattern: Rust idiom is to wrap every 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.
  • Self-contained testing: This example uses #[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

  • Minimize unsafe: Find the smallest possible unsafe region in the source and verify that all safe code is outside the unsafe block.
  • Safe alternative: Identify if a safe alternative exists for the demonstrated technique (e.g., bytemuck for transmute, CString for FFI strings) and implement it.
  • SAFETY documentation: Write a complete SAFETY comment for each unsafe block listing preconditions, invariants, and what would break if violated.
  • Open Source Repos