๐Ÿฆ€ Functional Rust

711: #[no_mangle] Exporting Rust Functions to C

Difficulty: 4 Level: Expert Emit stable C-ABI symbols from Rust with `#[no_mangle] pub extern "C" fn` โ€” the entry point for Rust-as-a-library.

The Problem This Solves

Rust normally mangles every function name to encode its full path, generic parameters, and crate version. `add` in crate `math` compiled at version 1.2.3 becomes something like `_ZN4math3add17hd3b3f2c1e4a5b6c7E`. That's great for Rust-to-Rust linking but completely opaque to C, Python, Node.js, or any other language consuming a shared library. `#[no_mangle]` tells the compiler to emit the symbol exactly as written in source โ€” `rust_add` stays `rust_add`. Combined with `pub extern "C"` on the function signature, you get a stable, C-ABI-compatible symbol that any C program can call with a normal `#include` and link. The ABI boundary is a contract: no panics, no Rust types in the signature, no `Result`, no `String`, no `Vec`. If any of these leak across the boundary, the C caller has no way to interpret them. Panics across FFI are undefined behaviour. The discipline is: convert everything to C types (`c_int`, `const c_char`, `mut T`) at the boundary, and convert errors to return codes, not `Result`.

The Intuition

Think of `#[no_mangle] pub extern "C" fn` as building a door between your Rust code and the outside world. The door has a specific frame (the C ABI) that everything must fit through: plain integers, raw pointers, and void. Rust's rich types live inside the house. The door exports a simplified view that C understands. `cbindgen` can auto-generate the corresponding C header from your Rust function signatures.

How It Works in Rust

use std::os::raw::c_int;

/// Exported as symbol `rust_add` โ€” callable from C as:
///   extern int rust_add(int a, int b);
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
 a + b
}

/// For string output, use *mut c_char + length, never Rust String.
#[no_mangle]
pub extern "C" fn rust_version(buf: *mut u8, len: usize) -> c_int {
 let version = b"1.0.0";
 if buf.is_null() || len < version.len() { return -1; }
 unsafe {
     // SAFETY: buf is non-null and len is large enough.
     std::ptr::copy_nonoverlapping(version.as_ptr(), buf, version.len());
 }
 version.len() as c_int
}
In `Cargo.toml`, set `crate-type = ["cdylib"]` for a shared library or `["staticlib"]` for a static archive. Run `cbindgen --crate my_lib --output my_lib.h` to generate the C header. Key rule: never panic across the FFI boundary. Wrap any panic-prone code in `std::panic::catch_unwind` and convert to a return code.

What This Unlocks

Key Differences

ConceptOCamlRust
Export to C`Callback.register` / `-output-obj``#[no_mangle] pub extern "C" fn`
Name manglingModule prefix added`#[no_mangle]` disables it
ABIOCaml calling convention`extern "C"` = System V / cdecl
Header generationManual`cbindgen` auto-generates `.h`
Crate type`ocamlopt -output-obj``cdylib` or `staticlib`
Panic across boundaryUndefined (setjmp/longjmp)UB โ€” must use `catch_unwind`
//! 711 โ€” #[no_mangle] Exporting Rust Functions to C
//! Stable C-ABI exports: the Rust-as-a-library pattern.

use std::os::raw::{c_char, c_int};

/// Add two integers โ€” exported as symbol `rust_add`.
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
    a + b
}

/// Compute nth Fibonacci number โ€” exported as `rust_fib`.
#[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
}

/// Compute absolute value โ€” exported as `rust_abs`.
#[no_mangle]
pub extern "C" fn rust_abs(n: c_int) -> c_int {
    n.abs()
}

/// Return a static C string constant โ€” exported as `rust_version`.
/// Caller must NOT free this pointer.
#[no_mangle]
pub extern "C" fn rust_version() -> *const c_char {
    b"1.0.0\0".as_ptr() as *const c_char
}

/// Sum of array โ€” exported as `rust_sum_slice`.
/// # Safety: ptr must be valid for len elements.
#[no_mangle]
pub unsafe extern "C" fn rust_sum_slice(ptr: *const i64, len: usize) -> i64 {
    if ptr.is_null() || len == 0 { return 0; }
    // SAFETY: caller guarantees ptr valid for len i64 elements.
    std::slice::from_raw_parts(ptr, len).iter().sum()
}

// โ”€โ”€ Call our own exports via extern "C" to demonstrate round-trip โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

// rust_add, rust_fib, rust_version defined above โ€” call directly.

fn main() {
    let sum = unsafe { rust_add(10, 32) };
    println!("rust_add(10, 32) = {sum}");

    let fibs: Vec<i32> = (0..10).map(|i| rust_fib(i)).collect();
    println!("fib(0..10) = {:?}", fibs);

    let ver = unsafe {
        std::ffi::CStr::from_ptr(rust_version()).to_str().unwrap()
    };
    println!("version = {ver}");

    let data = [1i64, 2, 3, 4, 5];
    let s = unsafe { rust_sum_slice(data.as_ptr(), data.len()) };
    println!("sum([1..5]) = {s}");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() { assert_eq!(rust_add(5, -2), 3); }
    #[test]
    fn test_fib() {
        assert_eq!(rust_fib(0), 0);
        assert_eq!(rust_fib(1), 1);
        assert_eq!(rust_fib(10), 55);
    }
    #[test]
    fn test_abs() { assert_eq!(rust_abs(-42), 42); assert_eq!(rust_abs(0), 0); }
    #[test]
    fn test_sum_slice() {
        let d = [10i64, 20, 30];
        let s = unsafe { rust_sum_slice(d.as_ptr(), d.len()) };
        assert_eq!(s, 60);
    }
}
(* OCaml: exporting to C via Callback.register or output-obj compilation. *)

(* In real OCaml-C interop, you'd compile with:
   ocamlfind ocamlopt -package ... -output-obj -o rust_funcs.o funcs.ml
   Then link into a C program.
   
   The Callback.register mechanism lets C call back into OCaml: *)

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)
  );
  (* C would call: caml_callback2(*caml_named_value("rust_add"), Val_int(3), Val_int(4)) *)
  print_endline "Callbacks registered for C interop"