ExamplesBy LevelBy TopicLearning Paths
711 Fundamental

no mangle export

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: no mangle export
  • • 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;
    
    /// Add two integers — symbol emitted as `rust_add` with C ABI.
    #[no_mangle]
    pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
        a + b
    }
    
    /// Fibonacci, iterative. Returns -1 for negative input (C-style error code).
    #[no_mangle]
    pub extern "C" fn rust_fib(n: c_int) -> c_int {
        if n < 0 { return -1; }
        if n <= 1 { return n; }
        let (mut a, mut b) = (0i32, 1i32);
        for _ in 2..=n {
            let c = a.wrapping_add(b);
            a = b; b = c;
        }
        b
    }

    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)]
    //! 711 — `#[no_mangle]` Exporting Rust Functions to C
    //!
    //! `#[no_mangle] pub extern "C" fn` is the declaration that turns a Rust
    //! function into a stable, C-ABI symbol any foreign language can call.
    //!
    //! Without `#[no_mangle]`, the Rust compiler encodes the full module path,
    //! generic parameters, and a hash into the symbol name (name mangling), making
    //! it impossible for C to find the function by a known name.
    //!
    //! The ABI contract at the boundary: no Rust-only types (`String`, `Vec`,
    //! `Result`), no panics, only C-compatible scalars and raw pointers.
    
    use std::os::raw::{c_char, c_int};
    
    // ── Exported symbols ──────────────────────────────────────────────────────────
    
    /// Add two C integers. Exported as the bare symbol `rust_add`.
    ///
    /// The `extern "C"` qualifier switches from Rust's default calling convention
    /// to the platform C ABI so that C callers can push/pop arguments as expected.
    #[no_mangle]
    pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
        a + b
    }
    
    /// Compute the nth Fibonacci number. Returns -1 for negative input.
    ///
    /// Uses an iterative accumulator — a direct loop is cleaner than an iterator
    /// chain here because we need two mutable bindings updated in lockstep.
    #[no_mangle]
    pub extern "C" fn rust_fib(n: c_int) -> c_int {
        if n < 0 {
            return -1;
        }
        if n <= 1 {
            return n;
        }
        let (mut a, mut b) = (0i32, 1i32);
        for _ in 2..=n {
            let c = a.wrapping_add(b);
            a = b;
            b = c;
        }
        b
    }
    
    /// Absolute value of a C integer.
    #[no_mangle]
    pub extern "C" fn rust_abs(n: c_int) -> c_int {
        n.abs()
    }
    
    /// Return a pointer to a static, null-terminated version string.
    ///
    /// SAFETY for the C caller: the pointer is valid for the lifetime of the
    /// process (it points to a `'static` byte literal), is null-terminated, and
    /// must not be mutated or freed. These invariants are documented in the
    /// accompanying C header.
    #[no_mangle]
    pub extern "C" fn rust_version() -> *const c_char {
        // `c"1.0.0"` is a C string literal (Rust 1.77+): null-terminated, stored
        // in `.rodata`, zero allocation. `.as_ptr()` yields `*const c_char`.
        c"1.0.0".as_ptr()
    }
    
    /// Clamp `value` to the inclusive range `[lo, hi]`.
    /// Returns `lo` when `value < lo`, `hi` when `value > hi`.
    /// If `lo > hi` the behaviour is unspecified (mirrors C's convention of
    /// leaving invalid ranges to the caller).
    #[no_mangle]
    pub extern "C" fn rust_clamp(value: c_int, lo: c_int, hi: c_int) -> c_int {
        value.clamp(lo, hi)
    }
    
    // ── Tests ─────────────────────────────────────────────────────────────────────
    // We call the `#[no_mangle]` functions directly from Rust — the C ABI is
    // transparent to the Rust test runner, which links and calls them normally.
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::ffi::CStr;
    
        #[test]
        fn test_add_basic() {
            assert_eq!(rust_add(3, 4), 7);
            assert_eq!(rust_add(0, 0), 0);
            assert_eq!(rust_add(-5, 5), 0);
            assert_eq!(rust_add(-3, -4), -7);
        }
    
        #[test]
        fn test_fib_sequence() {
            // F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(10)=55
            assert_eq!(rust_fib(0), 0);
            assert_eq!(rust_fib(1), 1);
            assert_eq!(rust_fib(2), 1);
            assert_eq!(rust_fib(5), 5);
            assert_eq!(rust_fib(10), 55);
        }
    
        #[test]
        fn test_fib_negative_returns_sentinel() {
            assert_eq!(rust_fib(-1), -1);
            assert_eq!(rust_fib(-100), -1);
        }
    
        #[test]
        fn test_abs() {
            assert_eq!(rust_abs(0), 0);
            assert_eq!(rust_abs(42), 42);
            assert_eq!(rust_abs(-42), 42);
            assert_eq!(rust_abs(i32::MAX), i32::MAX);
        }
    
        #[test]
        fn test_version_is_valid_c_string() {
            let ptr = rust_version();
            assert!(!ptr.is_null());
            // SAFETY: rust_version() returns a pointer to a 'static null-terminated
            // byte literal. It is valid, non-null, and properly terminated.
            let s = unsafe { CStr::from_ptr(ptr) };
            assert_eq!(s.to_str().unwrap(), "1.0.0");
        }
    
        #[test]
        fn test_clamp_within_and_out_of_range() {
            assert_eq!(rust_clamp(5, 0, 10), 5);
            assert_eq!(rust_clamp(-1, 0, 10), 0);
            assert_eq!(rust_clamp(15, 0, 10), 10);
            assert_eq!(rust_clamp(0, 0, 0), 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::ffi::CStr;
    
        #[test]
        fn test_add_basic() {
            assert_eq!(rust_add(3, 4), 7);
            assert_eq!(rust_add(0, 0), 0);
            assert_eq!(rust_add(-5, 5), 0);
            assert_eq!(rust_add(-3, -4), -7);
        }
    
        #[test]
        fn test_fib_sequence() {
            // F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(10)=55
            assert_eq!(rust_fib(0), 0);
            assert_eq!(rust_fib(1), 1);
            assert_eq!(rust_fib(2), 1);
            assert_eq!(rust_fib(5), 5);
            assert_eq!(rust_fib(10), 55);
        }
    
        #[test]
        fn test_fib_negative_returns_sentinel() {
            assert_eq!(rust_fib(-1), -1);
            assert_eq!(rust_fib(-100), -1);
        }
    
        #[test]
        fn test_abs() {
            assert_eq!(rust_abs(0), 0);
            assert_eq!(rust_abs(42), 42);
            assert_eq!(rust_abs(-42), 42);
            assert_eq!(rust_abs(i32::MAX), i32::MAX);
        }
    
        #[test]
        fn test_version_is_valid_c_string() {
            let ptr = rust_version();
            assert!(!ptr.is_null());
            // SAFETY: rust_version() returns a pointer to a 'static null-terminated
            // byte literal. It is valid, non-null, and properly terminated.
            let s = unsafe { CStr::from_ptr(ptr) };
            assert_eq!(s.to_str().unwrap(), "1.0.0");
        }
    
        #[test]
        fn test_clamp_within_and_out_of_range() {
            assert_eq!(rust_clamp(5, 0, 10), 5);
            assert_eq!(rust_clamp(-1, 0, 10), 0);
            assert_eq!(rust_clamp(15, 0, 10), 10);
            assert_eq!(rust_clamp(0, 0, 0), 0);
        }
    }

    Deep Comparison

    OCaml vs Rust: #[no_mangle] Exporting Functions to C

    Side-by-Side Code

    OCaml

    (* OCaml uses Callback.register to make functions callable from C.
       C code calls caml_named_value("rust_add") to get a closure handle,
       then invokes it with caml_callback2(..., Val_int(a), Val_int(b)).
       The GC, boxing, and tagging remain hidden from the C caller. *)
    
    let () =
      Callback.register "rust_add" (fun a b -> (a : int) + b);
      Callback.register "rust_fib" (fun n ->
        let rec fib k = if k <= 1 then k else fib (k-1) + fib (k-2) in
        fib (n : int)
      )
    

    Rust (idiomatic export)

    use std::os::raw::c_int;
    
    /// Add two integers — symbol emitted as `rust_add` with C ABI.
    #[no_mangle]
    pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
        a + b
    }
    
    /// Fibonacci, iterative. Returns -1 for negative input (C-style error code).
    #[no_mangle]
    pub extern "C" fn rust_fib(n: c_int) -> c_int {
        if n < 0 { return -1; }
        if n <= 1 { return n; }
        let (mut a, mut b) = (0i32, 1i32);
        for _ in 2..=n {
            let c = a.wrapping_add(b);
            a = b; b = c;
        }
        b
    }
    

    Rust (static string export)

    use std::os::raw::c_char;
    
    /// Return a pointer to a 'static, null-terminated version string.
    #[no_mangle]
    pub extern "C" fn rust_version() -> *const c_char {
        b"1.0.0\0".as_ptr().cast()
    }
    

    Type Signatures

    ConceptOCamlRust
    Export mechanismCallback.register "name" fn#[no_mangle] pub extern "C" fn name(...)
    Integer typeint (tagged boxed OCaml int)c_int = i32 on most platforms
    C stringNot natively supported; use Bytes + stubs*const c_char pointing to a b"...\0" literal
    Error signallingoption / result (OCaml-only)Return code convention (-1, NULL)
    Symbol visibilityNamed via runtime registryCompile-time symbol in the object file

    Key Insights

  • Name mangling vs stability. Rust mangles every symbol by default (path + hash) so that generic instantiations and crate versions can coexist. #[no_mangle] suppresses this, making the emitted symbol byte-for-byte what the source says — rust_addrust_add. OCaml never mangles in the same sense; instead it uses a runtime name registry (Callback.register) that C must navigate through the OCaml runtime API.
  • ABI contract. pub extern "C" switches from Rust's default (unspecified) calling convention to the platform C ABI: arguments on the stack or in registers in the C-defined order, return in the C-defined register. Without extern "C", the binary interface is undefined and C cannot reliably call the function even if the symbol is visible.
  • No Rust types across the boundary. OCaml's Callback mechanism still passes OCaml-boxed values (Val_int, caml_callback2); the C caller must understand the OCaml representation. Rust's FFI exports use only C primitives (c_int, *const c_char) — the C caller needs nothing Rust-specific at all. The discipline cost: no Result, no String, no panics — errors become return codes.
  • Static strings without allocation. b"1.0.0\0".as_ptr().cast::<c_char>() returns a pointer into the .rodata segment — zero heap cost, valid for the process lifetime. The C caller gets a const char * that needs no free. OCaml would require Bytes marshalling or a C stub with caml_copy_string.
  • Testing exported functions from Rust. #[no_mangle] pub extern "C" functions are still ordinary Rust functions at the type level; the Rust test runner calls them directly without going through the C ABI. This means you get full cargo test coverage on your exports with no extra test harness.
  • When to Use Each Style

    **Use #[no_mangle] pub extern "C" when:** building a shared library (cdylib) consumed by C, Python (ctypes/cffi), Node.js (ffi-napi), or any other language that can load native symbols. This is the standard "Rust as a library" pattern.

    **Use OCaml Callback.register when:** embedding OCaml inside a C host application and needing OCaml closures callable from C; the C side must link against the OCaml runtime and use caml_callback helpers rather than calling the function directly.

    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