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
- Rust as a drop-in C library: Any language with C FFI (Python via `ctypes`, Node via `ffi-napi`, Ruby via `fiddle`) can call your Rust code without a special binding layer.
- Incremental Rust adoption: Rewrite hot C functions in Rust one at a time, keeping the rest of the C codebase unchanged.
- `cbindgen` integration: Annotate Rust types and functions; auto-generate `.h` headers for C consumers โ the source of truth stays in Rust.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Export to C | `Callback.register` / `-output-obj` | `#[no_mangle] pub extern "C" fn` |
| Name mangling | Module prefix added | `#[no_mangle]` disables it |
| ABI | OCaml calling convention | `extern "C"` = System V / cdecl |
| Header generation | Manual | `cbindgen` auto-generates `.h` |
| Crate type | `ocamlopt -output-obj` | `cdylib` or `staticlib` |
| Panic across boundary | Undefined (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"